diff --git a/39/f0b252e2a9/0/content_hash.txt b/39/f0b252e2a9/0/content_hash.txt
new file mode 100644
index 000000000..bc838a5d8
--- /dev/null
+++ b/39/f0b252e2a9/0/content_hash.txt
@@ -0,0 +1 @@
+sha256:7ba0f6984cda06e68aade185647d616acc08b69d6c569702afca38f10670abcc
\ No newline at end of file
diff --git a/39/f0b252e2a9/0/context.md b/39/f0b252e2a9/0/context.md
new file mode 100644
index 000000000..bbc7ec4fa
--- /dev/null
+++ b/39/f0b252e2a9/0/context.md
@@ -0,0 +1,120 @@
+# Session Context
+
+## User Prompts
+
+### Prompt 1
+
+how datadir is used in the combined/
+
+### Prompt 2
+
+add support of providing a file of the sqlite storage if sqlite engine is specified in combined/ for store, authStore and activityStore
+
+### Prompt 3
+
+[Request interrupted by user for tool use]
+
+### Prompt 4
+
+we need to chevck if teh user provided query params to the file after ?
+
+### Prompt 5
+
+[Request interrupted by user for tool use]
+
+### Prompt 6
+
+question, do we need to do the query separation for activity store?
+
+### Prompt 7
+
+summarize the changes in a short pr description
+
+### Prompt 8
+
+Verify each finding against the current code and only fix it if needed.
+
+In `@combined/cmd/config.go` around lines 572 - 574, The assignment of
+c.Server.AuthStore.File to authStorageFile should resolve relative paths against
+mgmt.DataDir so the auth DB lives under the management data directory like the
+other sqlite stores; update the logic where authStorageFile is set (referencing
+c.Server.AuthStore.File and authStorageFile) to check if the value is non-empty
+and not an absolute path (use filepa...
+
+### Prompt 9
+
+Relative paths like "custom_idp.db" are now resolved against mgmt.DataDir
+
+### Prompt 10
+
+decode this dex user id: REDACTED
+
+### Prompt 11
+
+this one REDACTED and this one e1badda4-2a65-458c-aca0-b32c8e2b8a77
+
+### Prompt 12
+
+[Request interrupted by user]
+
+### Prompt 13
+
+this one REDACTED
+
+### Prompt 14
+
+decode these users, the first column is id: REDACTED|d67qqb69kmnc73b2nbm0|owner|0|0||[]|0|0|2026-02-22 14:49:17.460665591+00:00|2026-02-13 23:01:00.235815714+00:00|api|0||REDACTED|REDACTED
+REDACTED|d67qqb69kmnc73b2nbm0|admin|0|0||[]|...
+
+### Prompt 15
+
+add the original stored id
+
+### Prompt 16
+
+add a column after decoding base64
+
+### Prompt 17
+
+in the // Returns the type prefix, or "oidc" if no known prefix is found.
+func extractIdpType(connectorID string) string {
+ idx := strings.LastIndex(connectorID, "-")
+ if idx <= 0 {
+ return "oidc"
+ }
+ return strings.ToLower(connectorID[:idx])
+} in management/server/metrics/selfhosted.go I think that we don't count local or maybe counting it wrong. Could you please check it, fix it and add a test?
+
+### Prompt 18
+
+liek before, decode these: REDACTED
+REDACTED
+REDACTED
+REDACTED
+REDACTED
+REDACTED...
+
+### Prompt 19
+
+if there is no prefix - this is generic oidc: func generateIdentityProviderID(idpType types.IdentityProviderType) string {
+ id := xid.New().String()
+
+ switch idpType {
+ case types.IdentityProviderTypeOkta:
+ return "okta-" + id
+ case types.IdentityProviderTypeZitadel:
+ return "zitadel-" + id
+ case types.IdentityProviderTypeEntra:
+ return "entra-" + id
+ case types.IdentityProviderTypeGoogle:
+ return "google-" + id
+ case types.IdentityProviderTypePoc...
+
+### Prompt 20
+
+you need to test generateProperties() with real dex-encoded userids too
+
+### Prompt 21
+
+create a pr description and a title
+
diff --git a/39/f0b252e2a9/0/full.jsonl b/39/f0b252e2a9/0/full.jsonl
new file mode 100644
index 000000000..b638401a4
--- /dev/null
+++ b/39/f0b252e2a9/0/full.jsonl
@@ -0,0 +1,479 @@
+{"parentUuid":null,"isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","type":"progress","data":{"type":"hook_progress","hookEvent":"SessionStart","hookName":"SessionStart:startup","command":"entire hooks claude-code session-start"},"parentToolUseID":"cd372eeb-6091-4f2c-8eae-697f9e8a634f","toolUseID":"cd372eeb-6091-4f2c-8eae-697f9e8a634f","timestamp":"2026-03-02T18:55:00.396Z","uuid":"31e2a810-6ffd-4550-a9e8-91061c8f38da"}
+{"type":"file-history-snapshot","messageId":"14527d28-9cb5-477f-ab89-7959c931259f","snapshot":{"messageId":"14527d28-9cb5-477f-ab89-7959c931259f","trackedFileBackups":{},"timestamp":"2026-03-02T18:56:24.242Z"},"isSnapshotUpdate":false}
+{"parentUuid":"31e2a810-6ffd-4550-a9e8-91061c8f38da","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","type":"user","message":{"role":"user","content":"how datadir is used in the combined/ "},"uuid":"14527d28-9cb5-477f-ab89-7959c931259f","timestamp":"2026-03-02T18:56:23.163Z","permissionMode":"default"}
+{"parentUuid":"14527d28-9cb5-477f-ab89-7959c931259f","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","message":{"model":"claude-opus-4-6","id":"msg_015wuzWcWzH97CDEWeHTfjcc","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user wants to understand how `datadir` is used in the `combined/` directory. Let me explore that directory.","signature":"EpcCCkYICxgCKkCL4jhtBFp08zZh9bFv1ljCtVArRnVyiiXOoeehJ9XW3qS5LHs1LxkRFysNCbUyzEYTPIPpKkiFCmBCzCLgZSMoEgwTUpyT/3ogcbVvwHwaDONIMpa1fHILPd831CIw6VujLOCIDYHzd5hOPY+TCAG+H7Dh7Mk2UvtECJp3qtL8HLY6VNqo9zdO25n54GgTKn+7Fvtg7gOkabbsmXttXn1aBKA+TecrUzo5aiVwXsrgC15d+6YNJxthYKCU1rqcMBI4utFpr0Ln6MkkctlPF49PNvLJRlRQXKNf0aX4evGktqSXModpmjri3Czga0kNGVf0ii06W9iyEWoAM5UWTKIhSmXUrV4GjFFkkrez66XgGAE="}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":9904,"cache_read_input_tokens":10426,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":9904},"output_tokens":11,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYemFJ9cJh2ue1u1m41h4","type":"assistant","uuid":"095b279a-d48f-4fca-9672-f976ce417de3","timestamp":"2026-03-02T18:56:29.257Z"}
+{"parentUuid":"095b279a-d48f-4fca-9672-f976ce417de3","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","message":{"model":"claude-opus-4-6","id":"msg_015wuzWcWzH97CDEWeHTfjcc","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_016XWGFKaFVRWzfti9Nn4icM","name":"Agent","input":{"description":"Explore datadir in combined/","prompt":"Search the /Users/misha/Documents/GolandProjects/netbird/netbird/combined/ directory thoroughly for how \"datadir\" (case-insensitive) is used. Search for patterns like \"datadir\", \"dataDir\", \"data-dir\", \"DataDir\", \"data_dir\". Show the relevant code snippets with context. Also look at how config paths and data directories are constructed and passed around.","subagent_type":"Explore"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":9904,"cache_read_input_tokens":10426,"output_tokens":227,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":9904,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYemFJ9cJh2ue1u1m41h4","type":"assistant","uuid":"d0838330-7a60-4a59-ad82-7a56dc9816ed","timestamp":"2026-03-02T18:56:31.988Z"}
+{"parentUuid":"d0838330-7a60-4a59-ad82-7a56dc9816ed","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","type":"progress","data":{"type":"hook_progress","hookEvent":"PreToolUse","hookName":"PreToolUse:Agent","command":"entire hooks claude-code pre-task"},"toolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"919c735a-c3d6-4ac2-b846-c8da4f638693","timestamp":"2026-03-02T18:56:31.994Z"}
+{"parentUuid":"919c735a-c3d6-4ac2-b846-c8da4f638693","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Search the /Users/misha/Documents/GolandProjects/netbird/netbird/combined/ directory thoroughly for how \"datadir\" (case-insensitive) is used. Search for patterns like \"datadir\", \"dataDir\", \"data-dir\", \"DataDir\", \"data_dir\". Show the relevant code snippets with context. Also look at how config paths and data directories are constructed and passed around."}]},"uuid":"0e9493f1-2848-44d6-baef-55e52b690eb2","timestamp":"2026-03-02T18:56:32.350Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"Search the /Users/misha/Documents/GolandProjects/netbird/netbird/combined/ directory thoroughly for how \"datadir\" (case-insensitive) is used. Search for patterns like \"datadir\", \"dataDir\", \"data-dir\", \"DataDir\", \"data_dir\". Show the relevant code snippets with context. Also look at how config paths and data directories are constructed and passed around.","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"e38d953f-8baf-43c2-b194-99d1734d6491","timestamp":"2026-03-02T18:56:32.351Z"}
+{"parentUuid":"e38d953f-8baf-43c2-b194-99d1734d6491","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"assistant","timestamp":"2026-03-02T18:56:33.924Z","message":{"model":"claude-haiku-4-5-20251001","id":"msg_019PNWWyuqdAx3TQhLrGnAN9","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01PwrjSSq9FLDqkSCBELEyRb","name":"Glob","input":{"pattern":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/**/*"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":11216,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":11216},"output_tokens":4,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"requestId":"req_011CYemFrEfGPCJS4Umc7N4p","uuid":"153a61f4-05b3-487e-a92f-a8839db3ba4d"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"8ca55868-0793-4075-93ca-f92783222d9e","timestamp":"2026-03-02T18:56:33.925Z"}
+{"parentUuid":"8ca55868-0793-4075-93ca-f92783222d9e","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"assistant","timestamp":"2026-03-02T18:56:34.439Z","message":{"model":"claude-haiku-4-5-20251001","id":"msg_019PNWWyuqdAx3TQhLrGnAN9","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01WZnVXey3PdVQooshM826bG","name":"Grep","input":{"pattern":"datadir|dataDir|data-dir|data_dir|DataDir","path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined","output_mode":"content"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":11216,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":11216},"output_tokens":4,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"requestId":"req_011CYemFrEfGPCJS4Umc7N4p","uuid":"eefa2b29-69b9-4ca4-b860-41291019a650"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"a621787e-4444-4ffe-8b27-28a3f8970f3a","timestamp":"2026-03-02T18:56:34.439Z"}
+{"parentUuid":"a621787e-4444-4ffe-8b27-28a3f8970f3a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01PwrjSSq9FLDqkSCBELEyRb","type":"tool_result","content":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/Dockerfile\n/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/pprof.go\n/Users/misha/Documents/GolandProjects/netbird/netbird/combined/main.go\n/Users/misha/Documents/GolandProjects/netbird/netbird/combined/Dockerfile.multistage\n/Users/misha/Documents/GolandProjects/netbird/netbird/combined/LICENSE\n/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/token.go\n/Users/misha/Documents/GolandProjects/netbird/netbird/combined/perftest/perftest\n/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go\n/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/root.go\n/Users/misha/Documents/GolandProjects/netbird/netbird/combined/config.yaml.example"}]},"uuid":"256d82ce-8e10-445c-b034-515315e53d3c","timestamp":"2026-03-02T18:56:34.754Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"8ec329db-7b9a-4022-98f0-7a605e89a2a2","timestamp":"2026-03-02T18:56:34.786Z"}
+{"parentUuid":"8ec329db-7b9a-4022-98f0-7a605e89a2a2","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"assistant","timestamp":"2026-03-02T18:56:34.790Z","message":{"model":"claude-haiku-4-5-20251001","id":"msg_019PNWWyuqdAx3TQhLrGnAN9","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_014WpnA6fkZ4eBdBCoRWYrZ8","name":"Grep","input":{"pattern":"config.*path|config.*dir|path.*config|dir.*config","path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined","output_mode":"content"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":11216,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":11216},"output_tokens":4,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"requestId":"req_011CYemFrEfGPCJS4Umc7N4p","uuid":"be03fe5a-e65d-40f7-9ebe-9c275fbb69e6"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"bd58dc1c-b0b0-47c7-ac45-6a0cbf2e338b","timestamp":"2026-03-02T18:56:34.790Z"}
+{"parentUuid":"bd58dc1c-b0b0-47c7-ac45-6a0cbf2e338b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01WZnVXey3PdVQooshM826bG","type":"tool_result","content":"combined/config.yaml.example:43: dataDir: \"\"\ncombined/config.yaml.example:52: dataDir: \"/var/lib/netbird/\"\ncombined/config.yaml.example:107: # Activity events store configuration (optional, defaults to sqlite in dataDir)\ncombined/config.yaml.example:112: # Auth (embedded IdP) store configuration (optional, defaults to sqlite3 in dataDir/idp.db)\ncombined/cmd/root.go:455:\t\t\tDataDir: tlsCfg.LetsEncrypt.DataDir,\ncombined/cmd/root.go:468:\t\tcertManager, err := encryption.CreateCertManager(tlsCfg.LetsEncrypt.DataDir, tlsCfg.LetsEncrypt.Domains...)\ncombined/cmd/root.go:613:\tlog.Infof(\" Data dir: %s\", cfg.Server.DataDir)\ncombined/cmd/root.go:651:\tlog.Infof(\" Data dir: %s\", cfg.Management.DataDir)\ncombined/cmd/token.go:46:\tdatadir := cfg.Management.DataDir\ncombined/cmd/token.go:49:\ts, err := store.NewStore(ctx, engine, datadir, nil, true)\ncombined/cmd/config.go:59:\tDataDir string `yaml:\"dataDir\"` // Data directory for all services\ncombined/cmd/config.go:88:\tDataDir string `yaml:\"dataDir\"`\ncombined/cmd/config.go:120:\tDataDir string `yaml:\"dataDir\"`\ncombined/cmd/config.go:195:\t\t\tDataDir: \"/var/lib/netbird/\",\ncombined/cmd/config.go:217:\t\t\tDataDir: \"/var/lib/netbird/\",\ncombined/cmd/config.go:338:\tif c.Management.DataDir == \"\" || c.Management.DataDir == \"/var/lib/netbird/\" {\ncombined/cmd/config.go:339:\t\tc.Management.DataDir = c.Server.DataDir\ncombined/cmd/config.go:439:\tif c.Server.DataDir == \"\" {\ncombined/cmd/config.go:440:\t\treturn fmt.Errorf(\"server.dataDir is required\")\ncombined/cmd/config.go:472:\t\tc.Server.TLS.LetsEncrypt.DataDir != \"\" &&\ncombined/cmd/config.go:570:\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\ncombined/cmd/config.go:678:\t\tDatadir: mgmt.DataDir,"}]},"uuid":"e8942b98-a95c-45a9-9f8e-41d43fd5d74d","timestamp":"2026-03-02T18:56:35.049Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"fbd24be7-41fb-4939-aeff-e6459617f5e1","timestamp":"2026-03-02T18:56:35.051Z"}
+{"parentUuid":"fbd24be7-41fb-4939-aeff-e6459617f5e1","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_014WpnA6fkZ4eBdBCoRWYrZ8","type":"tool_result","content":"combined/cmd/root.go:63:\trootCmd.PersistentFlags().StringVarP(&configPath, \"config\", \"c\", \"\", \"path to YAML configuration file (required)\")"}]},"uuid":"7b154d01-4985-4e82-bfbc-61d423b5d2f7","timestamp":"2026-03-02T18:56:35.372Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"0a61c1d0-442d-4e83-998b-abc126b2aa84","timestamp":"2026-03-02T18:56:35.373Z"}
+{"parentUuid":"0a61c1d0-442d-4e83-998b-abc126b2aa84","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"assistant","timestamp":"2026-03-02T18:56:36.889Z","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01Ma5P3ckdgNnDW1NbvXrVT3","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01DZhxRFoNEu3fZL8kaTo1Y8","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":1344,"cache_read_input_tokens":11216,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":1344},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"requestId":"req_011CYemG5e4ZCWdrCetYtMR9","uuid":"81606d05-eaff-4b98-bdd0-5b2bdea320c0"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"5e31adc1-80bc-4819-b552-948035bb172a","timestamp":"2026-03-02T18:56:36.889Z"}
+{"parentUuid":"5e31adc1-80bc-4819-b552-948035bb172a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"assistant","timestamp":"2026-03-02T18:56:37.002Z","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01Ma5P3ckdgNnDW1NbvXrVT3","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01DAkZFrYhGUsStiQFubvJ9x","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/root.go"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":1344,"cache_read_input_tokens":11216,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":1344},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"requestId":"req_011CYemG5e4ZCWdrCetYtMR9","uuid":"a5a8d4d6-e95e-4fef-91fd-840aad833e2e"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"5365350d-b90d-4d78-8ee0-2700caeb10e9","timestamp":"2026-03-02T18:56:37.003Z"}
+{"parentUuid":"5365350d-b90d-4d78-8ee0-2700caeb10e9","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01DAkZFrYhGUsStiQFubvJ9x","type":"tool_result","content":" 1→package cmd\n 2→\n 3→import (\n 4→\t\"context\"\n 5→\t\"crypto/sha256\"\n 6→\t\"crypto/tls\"\n 7→\t\"errors\"\n 8→\t\"fmt\"\n 9→\t\"net\"\n 10→\t\"net/http\"\n 11→\t\"os\"\n 12→\t\"os/signal\"\n 13→\t\"strconv\"\n 14→\t\"strings\"\n 15→\t\"sync\"\n 16→\t\"syscall\"\n 17→\t\"time\"\n 18→\n 19→\t\"github.com/coder/websocket\"\n 20→\t\"github.com/hashicorp/go-multierror\"\n 21→\tlog \"github.com/sirupsen/logrus\"\n 22→\t\"github.com/spf13/cobra\"\n 23→\t\"go.opentelemetry.io/otel/metric\"\n 24→\t\"google.golang.org/grpc\"\n 25→\n 26→\t\"github.com/netbirdio/netbird/encryption\"\n 27→\tmgmtServer \"github.com/netbirdio/netbird/management/internals/server\"\n 28→\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n 29→\t\"github.com/netbirdio/netbird/management/server/telemetry\"\n 30→\t\"github.com/netbirdio/netbird/relay/healthcheck\"\n 31→\trelayServer \"github.com/netbirdio/netbird/relay/server\"\n 32→\t\"github.com/netbirdio/netbird/relay/server/listener/ws\"\n 33→\tsharedMetrics \"github.com/netbirdio/netbird/shared/metrics\"\n 34→\t\"github.com/netbirdio/netbird/shared/relay/auth\"\n 35→\t\"github.com/netbirdio/netbird/shared/signal/proto\"\n 36→\tsignalServer \"github.com/netbirdio/netbird/signal/server\"\n 37→\t\"github.com/netbirdio/netbird/stun\"\n 38→\t\"github.com/netbirdio/netbird/util\"\n 39→\t\"github.com/netbirdio/netbird/util/wsproxy\"\n 40→\twsproxyserver \"github.com/netbirdio/netbird/util/wsproxy/server\"\n 41→)\n 42→\n 43→var (\n 44→\tconfigPath string\n 45→\tconfig *CombinedConfig\n 46→\n 47→\trootCmd = &cobra.Command{\n 48→\t\tUse: \"combined\",\n 49→\t\tShort: \"Combined Netbird server (Management + Signal + Relay + STUN)\",\n 50→\t\tLong: `Combined Netbird server for self-hosted deployments.\n 51→\n 52→All services (Management, Signal, Relay) are multiplexed on a single port.\n 53→Optional STUN server runs on separate UDP ports.\n 54→\n 55→Configuration is loaded from a YAML file specified with --config.`,\n 56→\t\tSilenceUsage: true,\n 57→\t\tSilenceErrors: true,\n 58→\t\tRunE: execute,\n 59→\t}\n 60→)\n 61→\n 62→func init() {\n 63→\trootCmd.PersistentFlags().StringVarP(&configPath, \"config\", \"c\", \"\", \"path to YAML configuration file (required)\")\n 64→\t_ = rootCmd.MarkPersistentFlagRequired(\"config\")\n 65→\n 66→\trootCmd.AddCommand(newTokenCommands())\n 67→}\n 68→\n 69→func Execute() error {\n 70→\treturn rootCmd.Execute()\n 71→}\n 72→\n 73→func waitForExitSignal() {\n 74→\tosSigs := make(chan os.Signal, 1)\n 75→\tsignal.Notify(osSigs, syscall.SIGINT, syscall.SIGTERM)\n 76→\t<-osSigs\n 77→}\n 78→\n 79→func execute(cmd *cobra.Command, _ []string) error {\n 80→\tif err := initializeConfig(); err != nil {\n 81→\t\treturn err\n 82→\t}\n 83→\n 84→\t// Management is required as the base server when signal or relay are enabled\n 85→\tif (config.Signal.Enabled || config.Relay.Enabled) && !config.Management.Enabled {\n 86→\t\treturn fmt.Errorf(\"management must be enabled when signal or relay are enabled (provides the base HTTP server)\")\n 87→\t}\n 88→\n 89→\tservers, err := createAllServers(cmd.Context(), config)\n 90→\tif err != nil {\n 91→\t\treturn err\n 92→\t}\n 93→\n 94→\t// Register services with management's gRPC server using AfterInit hook\n 95→\tsetupServerHooks(servers, config)\n 96→\n 97→\t// Start management server (this also starts the HTTP listener)\n 98→\tif servers.mgmtSrv != nil {\n 99→\t\tif err := servers.mgmtSrv.Start(cmd.Context()); err != nil {\n 100→\t\t\tcleanupSTUNListeners(servers.stunListeners)\n 101→\t\t\treturn fmt.Errorf(\"failed to start management server: %w\", err)\n 102→\t\t}\n 103→\t}\n 104→\n 105→\t// Start all other servers\n 106→\twg := sync.WaitGroup{}\n 107→\tstartServers(&wg, servers.relaySrv, servers.healthcheck, servers.stunServer, servers.metricsServer)\n 108→\n 109→\twaitForExitSignal()\n 110→\n 111→\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n 112→\tdefer cancel()\n 113→\n 114→\terr = shutdownServers(ctx, servers.relaySrv, servers.healthcheck, servers.stunServer, servers.mgmtSrv, servers.metricsServer)\n 115→\twg.Wait()\n 116→\treturn err\n 117→}\n 118→\n 119→// initializeConfig loads and validates the configuration, then initializes logging.\n 120→func initializeConfig() error {\n 121→\tvar err error\n 122→\tconfig, err = LoadConfig(configPath)\n 123→\tif err != nil {\n 124→\t\treturn fmt.Errorf(\"failed to load config: %w\", err)\n 125→\t}\n 126→\n 127→\tif err := config.Validate(); err != nil {\n 128→\t\treturn fmt.Errorf(\"invalid config: %w\", err)\n 129→\t}\n 130→\n 131→\tif err := util.InitLog(config.Server.LogLevel, config.Server.LogFile); err != nil {\n 132→\t\treturn fmt.Errorf(\"failed to initialize log: %w\", err)\n 133→\t}\n 134→\n 135→\tif dsn := config.Server.Store.DSN; dsn != \"\" {\n 136→\t\tswitch strings.ToLower(config.Server.Store.Engine) {\n 137→\t\tcase \"postgres\":\n 138→\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n 139→\t\tcase \"mysql\":\n 140→\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n 141→\t\t}\n 142→\t}\n 143→\n 144→\tif engine := config.Server.ActivityStore.Engine; engine != \"\" {\n 145→\t\tengineLower := strings.ToLower(engine)\n 146→\t\tif engineLower == \"postgres\" && config.Server.ActivityStore.DSN == \"\" {\n 147→\t\t\treturn fmt.Errorf(\"activityStore.dsn is required when activityStore.engine is postgres\")\n 148→\t\t}\n 149→\t\tos.Setenv(\"NB_ACTIVITY_EVENT_STORE_ENGINE\", engineLower)\n 150→\t\tif dsn := config.Server.ActivityStore.DSN; dsn != \"\" {\n 151→\t\t\tos.Setenv(\"NB_ACTIVITY_EVENT_POSTGRES_DSN\", dsn)\n 152→\t\t}\n 153→\t}\n 154→\n 155→\tlog.Infof(\"Starting combined NetBird server\")\n 156→\tlogConfig(config)\n 157→\tlogEnvVars()\n 158→\treturn nil\n 159→}\n 160→\n 161→// serverInstances holds all server instances created during startup.\n 162→type serverInstances struct {\n 163→\trelaySrv *relayServer.Server\n 164→\tmgmtSrv *mgmtServer.BaseServer\n 165→\tsignalSrv *signalServer.Server\n 166→\thealthcheck *healthcheck.Server\n 167→\tstunServer *stun.Server\n 168→\tstunListeners []*net.UDPConn\n 169→\tmetricsServer *sharedMetrics.Metrics\n 170→}\n 171→\n 172→// createAllServers creates all server instances based on configuration.\n 173→func createAllServers(ctx context.Context, cfg *CombinedConfig) (*serverInstances, error) {\n 174→\tmetricsServer, err := sharedMetrics.NewServer(cfg.Server.MetricsPort, \"\")\n 175→\tif err != nil {\n 176→\t\treturn nil, fmt.Errorf(\"failed to create metrics server: %w\", err)\n 177→\t}\n 178→\tservers := &serverInstances{\n 179→\t\tmetricsServer: metricsServer,\n 180→\t}\n 181→\n 182→\t_, tlsSupport, err := handleTLSConfig(cfg)\n 183→\tif err != nil {\n 184→\t\treturn nil, fmt.Errorf(\"failed to setup TLS config: %w\", err)\n 185→\t}\n 186→\n 187→\tif err := servers.createRelayServer(cfg, tlsSupport); err != nil {\n 188→\t\treturn nil, err\n 189→\t}\n 190→\n 191→\tif err := servers.createManagementServer(ctx, cfg); err != nil {\n 192→\t\treturn nil, err\n 193→\t}\n 194→\n 195→\tif err := servers.createSignalServer(ctx, cfg); err != nil {\n 196→\t\treturn nil, err\n 197→\t}\n 198→\n 199→\tif err := servers.createHealthcheckServer(cfg); err != nil {\n 200→\t\treturn nil, err\n 201→\t}\n 202→\n 203→\treturn servers, nil\n 204→}\n 205→\n 206→func (s *serverInstances) createRelayServer(cfg *CombinedConfig, tlsSupport bool) error {\n 207→\tif !cfg.Relay.Enabled {\n 208→\t\treturn nil\n 209→\t}\n 210→\n 211→\tvar err error\n 212→\ts.stunListeners, err = createSTUNListeners(cfg)\n 213→\tif err != nil {\n 214→\t\treturn err\n 215→\t}\n 216→\n 217→\thashedSecret := sha256.Sum256([]byte(cfg.Relay.AuthSecret))\n 218→\tauthenticator := auth.NewTimedHMACValidator(hashedSecret[:], 24*time.Hour)\n 219→\n 220→\trelayCfg := relayServer.Config{\n 221→\t\tMeter: s.metricsServer.Meter,\n 222→\t\tExposedAddress: cfg.Relay.ExposedAddress,\n 223→\t\tAuthValidator: authenticator,\n 224→\t\tTLSSupport: tlsSupport,\n 225→\t}\n 226→\n 227→\ts.relaySrv, err = createRelayServer(relayCfg, s.stunListeners)\n 228→\tif err != nil {\n 229→\t\treturn err\n 230→\t}\n 231→\n 232→\tlog.Infof(\"Relay server created\")\n 233→\n 234→\tif len(s.stunListeners) > 0 {\n 235→\t\ts.stunServer = stun.NewServer(s.stunListeners, cfg.Relay.Stun.LogLevel)\n 236→\t}\n 237→\n 238→\treturn nil\n 239→}\n 240→\n 241→func (s *serverInstances) createManagementServer(ctx context.Context, cfg *CombinedConfig) error {\n 242→\tif !cfg.Management.Enabled {\n 243→\t\treturn nil\n 244→\t}\n 245→\n 246→\tmgmtConfig, err := cfg.ToManagementConfig()\n 247→\tif err != nil {\n 248→\t\treturn fmt.Errorf(\"failed to create management config: %w\", err)\n 249→\t}\n 250→\n 251→\t_, portStr, portErr := net.SplitHostPort(cfg.Server.ListenAddress)\n 252→\tif portErr != nil {\n 253→\t\tportStr = \"443\"\n 254→\t}\n 255→\tmgmtPort, _ := strconv.Atoi(portStr)\n 256→\n 257→\tif err := ApplyEmbeddedIdPConfig(ctx, mgmtConfig, mgmtPort, false); err != nil {\n 258→\t\tcleanupSTUNListeners(s.stunListeners)\n 259→\t\treturn fmt.Errorf(\"failed to apply embedded IdP config: %w\", err)\n 260→\t}\n 261→\n 262→\tif err := EnsureEncryptionKey(ctx, mgmtConfig); err != nil {\n 263→\t\tcleanupSTUNListeners(s.stunListeners)\n 264→\t\treturn fmt.Errorf(\"failed to ensure encryption key: %w\", err)\n 265→\t}\n 266→\n 267→\tLogConfigInfo(mgmtConfig)\n 268→\n 269→\ts.mgmtSrv, err = createManagementServer(cfg, mgmtConfig)\n 270→\tif err != nil {\n 271→\t\tcleanupSTUNListeners(s.stunListeners)\n 272→\t\treturn fmt.Errorf(\"failed to create management server: %w\", err)\n 273→\t}\n 274→\n 275→\t// Inject externally-managed AppMetrics so management uses the shared metrics server\n 276→\tappMetrics, err := telemetry.NewAppMetricsWithMeter(ctx, s.metricsServer.Meter)\n 277→\tif err != nil {\n 278→\t\tcleanupSTUNListeners(s.stunListeners)\n 279→\t\treturn fmt.Errorf(\"failed to create management app metrics: %w\", err)\n 280→\t}\n 281→\tmgmtServer.Inject[telemetry.AppMetrics](s.mgmtSrv, appMetrics)\n 282→\n 283→\tlog.Infof(\"Management server created\")\n 284→\treturn nil\n 285→}\n 286→\n 287→func (s *serverInstances) createSignalServer(ctx context.Context, cfg *CombinedConfig) error {\n 288→\tif !cfg.Signal.Enabled {\n 289→\t\treturn nil\n 290→\t}\n 291→\n 292→\tvar err error\n 293→\ts.signalSrv, err = signalServer.NewServer(ctx, s.metricsServer.Meter, \"signal_\")\n 294→\tif err != nil {\n 295→\t\tcleanupSTUNListeners(s.stunListeners)\n 296→\t\treturn fmt.Errorf(\"failed to create signal server: %w\", err)\n 297→\t}\n 298→\n 299→\tlog.Infof(\"Signal server created\")\n 300→\treturn nil\n 301→}\n 302→\n 303→func (s *serverInstances) createHealthcheckServer(cfg *CombinedConfig) error {\n 304→\thCfg := healthcheck.Config{\n 305→\t\tListenAddress: cfg.Server.HealthcheckAddress,\n 306→\t\tServiceChecker: s.relaySrv,\n 307→\t}\n 308→\n 309→\tvar err error\n 310→\ts.healthcheck, err = createHealthCheck(hCfg, s.stunListeners)\n 311→\treturn err\n 312→}\n 313→\n 314→// setupServerHooks registers services with management's gRPC server.\n 315→func setupServerHooks(servers *serverInstances, cfg *CombinedConfig) {\n 316→\tif servers.mgmtSrv == nil {\n 317→\t\treturn\n 318→\t}\n 319→\n 320→\tservers.mgmtSrv.AfterInit(func(s *mgmtServer.BaseServer) {\n 321→\t\tgrpcSrv := s.GRPCServer()\n 322→\n 323→\t\tif servers.signalSrv != nil {\n 324→\t\t\tproto.RegisterSignalExchangeServer(grpcSrv, servers.signalSrv)\n 325→\t\t\tlog.Infof(\"Signal server registered on port %s\", cfg.Server.ListenAddress)\n 326→\t\t}\n 327→\n 328→\t\ts.SetHandlerFunc(createCombinedHandler(grpcSrv, s.APIHandler(), servers.relaySrv, servers.metricsServer.Meter, cfg))\n 329→\t\tif servers.relaySrv != nil {\n 330→\t\t\tlog.Infof(\"Relay WebSocket handler added (path: /relay)\")\n 331→\t\t}\n 332→\t})\n 333→}\n 334→\n 335→func startServers(wg *sync.WaitGroup, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, metricsServer *sharedMetrics.Metrics) {\n 336→\tif srv != nil {\n 337→\t\tinstanceURL := srv.InstanceURL()\n 338→\t\tlog.Infof(\"Relay server instance URL: %s\", instanceURL.String())\n 339→\t\tlog.Infof(\"Relay WebSocket multiplexed on management port (no separate relay listener)\")\n 340→\t}\n 341→\n 342→\twg.Add(1)\n 343→\tgo func() {\n 344→\t\tdefer wg.Done()\n 345→\t\tlog.Infof(\"running metrics server: %s%s\", metricsServer.Addr, metricsServer.Endpoint)\n 346→\t\tif err := metricsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {\n 347→\t\t\tlog.Fatalf(\"failed to start metrics server: %v\", err)\n 348→\t\t}\n 349→\t}()\n 350→\n 351→\twg.Add(1)\n 352→\tgo func() {\n 353→\t\tdefer wg.Done()\n 354→\t\tif err := httpHealthcheck.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {\n 355→\t\t\tlog.Fatalf(\"failed to start healthcheck server: %v\", err)\n 356→\t\t}\n 357→\t}()\n 358→\n 359→\tif stunServer != nil {\n 360→\t\twg.Add(1)\n 361→\t\tgo func() {\n 362→\t\t\tdefer wg.Done()\n 363→\t\t\tif err := stunServer.Listen(); err != nil {\n 364→\t\t\t\tif errors.Is(err, stun.ErrServerClosed) {\n 365→\t\t\t\t\treturn\n 366→\t\t\t\t}\n 367→\t\t\t\tlog.Errorf(\"STUN server error: %v\", err)\n 368→\t\t\t}\n 369→\t\t}()\n 370→\t}\n 371→}\n 372→\n 373→func shutdownServers(ctx context.Context, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, mgmtSrv *mgmtServer.BaseServer, metricsServer *sharedMetrics.Metrics) error {\n 374→\tvar errs error\n 375→\n 376→\tif err := httpHealthcheck.Shutdown(ctx); err != nil {\n 377→\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close healthcheck server: %w\", err))\n 378→\t}\n 379→\n 380→\tif stunServer != nil {\n 381→\t\tif err := stunServer.Shutdown(); err != nil {\n 382→\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close STUN server: %w\", err))\n 383→\t\t}\n 384→\t}\n 385→\n 386→\tif srv != nil {\n 387→\t\tif err := srv.Shutdown(ctx); err != nil {\n 388→\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close relay server: %w\", err))\n 389→\t\t}\n 390→\t}\n 391→\n 392→\tif mgmtSrv != nil {\n 393→\t\tlog.Infof(\"shutting down management and signal servers\")\n 394→\t\tif err := mgmtSrv.Stop(); err != nil {\n 395→\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close management server: %w\", err))\n 396→\t\t}\n 397→\t}\n 398→\n 399→\tif metricsServer != nil {\n 400→\t\tlog.Infof(\"shutting down metrics server\")\n 401→\t\tif err := metricsServer.Shutdown(ctx); err != nil {\n 402→\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close metrics server: %w\", err))\n 403→\t\t}\n 404→\t}\n 405→\n 406→\treturn errs\n 407→}\n 408→\n 409→func createHealthCheck(hCfg healthcheck.Config, stunListeners []*net.UDPConn) (*healthcheck.Server, error) {\n 410→\thttpHealthcheck, err := healthcheck.NewServer(hCfg)\n 411→\tif err != nil {\n 412→\t\tcleanupSTUNListeners(stunListeners)\n 413→\t\treturn nil, fmt.Errorf(\"failed to create healthcheck server: %w\", err)\n 414→\t}\n 415→\treturn httpHealthcheck, nil\n 416→}\n 417→\n 418→func createRelayServer(cfg relayServer.Config, stunListeners []*net.UDPConn) (*relayServer.Server, error) {\n 419→\tsrv, err := relayServer.NewServer(cfg)\n 420→\tif err != nil {\n 421→\t\tcleanupSTUNListeners(stunListeners)\n 422→\t\treturn nil, fmt.Errorf(\"failed to create relay server: %w\", err)\n 423→\t}\n 424→\treturn srv, nil\n 425→}\n 426→\n 427→func cleanupSTUNListeners(stunListeners []*net.UDPConn) {\n 428→\tfor _, l := range stunListeners {\n 429→\t\t_ = l.Close()\n 430→\t}\n 431→}\n 432→\n 433→func createSTUNListeners(cfg *CombinedConfig) ([]*net.UDPConn, error) {\n 434→\tvar stunListeners []*net.UDPConn\n 435→\tif cfg.Relay.Stun.Enabled {\n 436→\t\tfor _, port := range cfg.Relay.Stun.Ports {\n 437→\t\t\tlistener, err := net.ListenUDP(\"udp\", &net.UDPAddr{Port: port})\n 438→\t\t\tif err != nil {\n 439→\t\t\t\tcleanupSTUNListeners(stunListeners)\n 440→\t\t\t\treturn nil, fmt.Errorf(\"failed to create STUN listener on port %d: %w\", port, err)\n 441→\t\t\t}\n 442→\t\t\tstunListeners = append(stunListeners, listener)\n 443→\t\t\tlog.Infof(\"STUN server listening on UDP port %d\", port)\n 444→\t\t}\n 445→\t}\n 446→\treturn stunListeners, nil\n 447→}\n 448→\n 449→func handleTLSConfig(cfg *CombinedConfig) (*tls.Config, bool, error) {\n 450→\ttlsCfg := cfg.Server.TLS\n 451→\n 452→\tif tlsCfg.LetsEncrypt.AWSRoute53 {\n 453→\t\tlog.Debugf(\"using Let's Encrypt DNS resolver with Route 53 support\")\n 454→\t\tr53 := encryption.Route53TLS{\n 455→\t\t\tDataDir: tlsCfg.LetsEncrypt.DataDir,\n 456→\t\t\tEmail: tlsCfg.LetsEncrypt.Email,\n 457→\t\t\tDomains: tlsCfg.LetsEncrypt.Domains,\n 458→\t\t}\n 459→\t\ttc, err := r53.GetCertificate()\n 460→\t\tif err != nil {\n 461→\t\t\treturn nil, false, err\n 462→\t\t}\n 463→\t\treturn tc, true, nil\n 464→\t}\n 465→\n 466→\tif cfg.HasLetsEncrypt() {\n 467→\t\tlog.Infof(\"setting up TLS with Let's Encrypt\")\n 468→\t\tcertManager, err := encryption.CreateCertManager(tlsCfg.LetsEncrypt.DataDir, tlsCfg.LetsEncrypt.Domains...)\n 469→\t\tif err != nil {\n 470→\t\t\treturn nil, false, fmt.Errorf(\"failed creating LetsEncrypt cert manager: %w\", err)\n 471→\t\t}\n 472→\t\treturn certManager.TLSConfig(), true, nil\n 473→\t}\n 474→\n 475→\tif cfg.HasTLSCert() {\n 476→\t\tlog.Debugf(\"using file based TLS config\")\n 477→\t\ttc, err := encryption.LoadTLSConfig(tlsCfg.CertFile, tlsCfg.KeyFile)\n 478→\t\tif err != nil {\n 479→\t\t\treturn nil, false, err\n 480→\t\t}\n 481→\t\treturn tc, true, nil\n 482→\t}\n 483→\n 484→\treturn nil, false, nil\n 485→}\n 486→\n 487→func createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (*mgmtServer.BaseServer, error) {\n 488→\tmgmt := cfg.Management\n 489→\n 490→\tdnsDomain := mgmt.DnsDomain\n 491→\tsingleAccModeDomain := dnsDomain\n 492→\n 493→\t// Extract port from listen address\n 494→\t_, portStr, err := net.SplitHostPort(cfg.Server.ListenAddress)\n 495→\tif err != nil {\n 496→\t\t// If no port specified, assume default\n 497→\t\tportStr = \"443\"\n 498→\t}\n 499→\tmgmtPort, _ := strconv.Atoi(portStr)\n 500→\n 501→\tmgmtSrv := mgmtServer.NewServer(\n 502→\t\t&mgmtServer.Config{\n 503→\t\t\tNbConfig: mgmtConfig,\n 504→\t\t\tDNSDomain: dnsDomain,\n 505→\t\t\tMgmtSingleAccModeDomain: singleAccModeDomain,\n 506→\t\t\tMgmtPort: mgmtPort,\n 507→\t\t\tMgmtMetricsPort: cfg.Server.MetricsPort,\n 508→\t\t\tDisableMetrics: mgmt.DisableAnonymousMetrics,\n 509→\t\t\tDisableGeoliteUpdate: mgmt.DisableGeoliteUpdate,\n 510→\t\t\t// Always enable user deletion from IDP in combined server (embedded IdP is always enabled)\n 511→\t\t\tUserDeleteFromIDPEnabled: true,\n 512→\t\t},\n 513→\t)\n 514→\n 515→\treturn mgmtSrv, nil\n 516→}\n 517→\n 518→// createCombinedHandler creates an HTTP handler that multiplexes Management, Signal (via wsproxy), and Relay WebSocket traffic\n 519→func createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, relaySrv *relayServer.Server, meter metric.Meter, cfg *CombinedConfig) http.Handler {\n 520→\twsProxy := wsproxyserver.New(grpcServer, wsproxyserver.WithOTelMeter(meter))\n 521→\n 522→\tvar relayAcceptFn func(conn net.Conn)\n 523→\tif relaySrv != nil {\n 524→\t\trelayAcceptFn = relaySrv.RelayAccept()\n 525→\t}\n 526→\n 527→\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n 528→\t\tswitch {\n 529→\t\t// Native gRPC traffic (HTTP/2 with gRPC content-type)\n 530→\t\tcase r.ProtoMajor == 2 && (strings.HasPrefix(r.Header.Get(\"Content-Type\"), \"application/grpc\") ||\n 531→\t\t\tstrings.HasPrefix(r.Header.Get(\"Content-Type\"), \"application/grpc+proto\")):\n 532→\t\t\tgrpcServer.ServeHTTP(w, r)\n 533→\n 534→\t\t// WebSocket proxy for Management gRPC\n 535→\t\tcase r.URL.Path == wsproxy.ProxyPath+wsproxy.ManagementComponent:\n 536→\t\t\twsProxy.Handler().ServeHTTP(w, r)\n 537→\n 538→\t\t// WebSocket proxy for Signal gRPC\n 539→\t\tcase r.URL.Path == wsproxy.ProxyPath+wsproxy.SignalComponent:\n 540→\t\t\tif cfg.Signal.Enabled {\n 541→\t\t\t\twsProxy.Handler().ServeHTTP(w, r)\n 542→\t\t\t} else {\n 543→\t\t\t\thttp.Error(w, \"Signal service not enabled\", http.StatusNotFound)\n 544→\t\t\t}\n 545→\n 546→\t\t// Relay WebSocket\n 547→\t\tcase r.URL.Path == \"/relay\":\n 548→\t\t\tif relayAcceptFn != nil {\n 549→\t\t\t\thandleRelayWebSocket(w, r, relayAcceptFn, cfg)\n 550→\t\t\t} else {\n 551→\t\t\t\thttp.Error(w, \"Relay service not enabled\", http.StatusNotFound)\n 552→\t\t\t}\n 553→\n 554→\t\t// Management HTTP API (default)\n 555→\t\tdefault:\n 556→\t\t\thttpHandler.ServeHTTP(w, r)\n 557→\t\t}\n 558→\t})\n 559→}\n 560→\n 561→// handleRelayWebSocket handles incoming WebSocket connections for the relay service\n 562→func handleRelayWebSocket(w http.ResponseWriter, r *http.Request, acceptFn func(conn net.Conn), cfg *CombinedConfig) {\n 563→\tacceptOptions := &websocket.AcceptOptions{\n 564→\t\tOriginPatterns: []string{\"*\"},\n 565→\t}\n 566→\n 567→\twsConn, err := websocket.Accept(w, r, acceptOptions)\n 568→\tif err != nil {\n 569→\t\tlog.Errorf(\"failed to accept relay ws connection: %s\", err)\n 570→\t\treturn\n 571→\t}\n 572→\n 573→\tconnRemoteAddr := r.RemoteAddr\n 574→\tif r.Header.Get(\"X-Real-Ip\") != \"\" && r.Header.Get(\"X-Real-Port\") != \"\" {\n 575→\t\tconnRemoteAddr = net.JoinHostPort(r.Header.Get(\"X-Real-Ip\"), r.Header.Get(\"X-Real-Port\"))\n 576→\t}\n 577→\n 578→\trAddr, err := net.ResolveTCPAddr(\"tcp\", connRemoteAddr)\n 579→\tif err != nil {\n 580→\t\t_ = wsConn.Close(websocket.StatusInternalError, \"internal error\")\n 581→\t\treturn\n 582→\t}\n 583→\n 584→\tlAddr, err := net.ResolveTCPAddr(\"tcp\", cfg.Server.ListenAddress)\n 585→\tif err != nil {\n 586→\t\t_ = wsConn.Close(websocket.StatusInternalError, \"internal error\")\n 587→\t\treturn\n 588→\t}\n 589→\n 590→\tlog.Debugf(\"Relay WS client connected from: %s\", rAddr)\n 591→\n 592→\tconn := ws.NewConn(wsConn, lAddr, rAddr)\n 593→\tacceptFn(conn)\n 594→}\n 595→\n 596→// logConfig prints all configuration parameters for debugging\n 597→func logConfig(cfg *CombinedConfig) {\n 598→\tlog.Info(\"=== Configuration ===\")\n 599→\tlogServerConfig(cfg)\n 600→\tlogComponentsConfig(cfg)\n 601→\tlogRelayConfig(cfg)\n 602→\tlogManagementConfig(cfg)\n 603→\tlog.Info(\"=== End Configuration ===\")\n 604→}\n 605→\n 606→func logServerConfig(cfg *CombinedConfig) {\n 607→\tlog.Info(\"--- Server ---\")\n 608→\tlog.Infof(\" Listen address: %s\", cfg.Server.ListenAddress)\n 609→\tlog.Infof(\" Exposed address: %s\", cfg.Server.ExposedAddress)\n 610→\tlog.Infof(\" Healthcheck address: %s\", cfg.Server.HealthcheckAddress)\n 611→\tlog.Infof(\" Metrics port: %d\", cfg.Server.MetricsPort)\n 612→\tlog.Infof(\" Log level: %s\", cfg.Server.LogLevel)\n 613→\tlog.Infof(\" Data dir: %s\", cfg.Server.DataDir)\n 614→\n 615→\tswitch {\n 616→\tcase cfg.HasTLSCert():\n 617→\t\tlog.Infof(\" TLS: cert=%s, key=%s\", cfg.Server.TLS.CertFile, cfg.Server.TLS.KeyFile)\n 618→\tcase cfg.HasLetsEncrypt():\n 619→\t\tlog.Infof(\" TLS: Let's Encrypt (domains=%v)\", cfg.Server.TLS.LetsEncrypt.Domains)\n 620→\tdefault:\n 621→\t\tlog.Info(\" TLS: disabled (using reverse proxy)\")\n 622→\t}\n 623→}\n 624→\n 625→func logComponentsConfig(cfg *CombinedConfig) {\n 626→\tlog.Info(\"--- Components ---\")\n 627→\tlog.Infof(\" Management: %v (log level: %s)\", cfg.Management.Enabled, cfg.Management.LogLevel)\n 628→\tlog.Infof(\" Signal: %v (log level: %s)\", cfg.Signal.Enabled, cfg.Signal.LogLevel)\n 629→\tlog.Infof(\" Relay: %v (log level: %s)\", cfg.Relay.Enabled, cfg.Relay.LogLevel)\n 630→}\n 631→\n 632→func logRelayConfig(cfg *CombinedConfig) {\n 633→\tif !cfg.Relay.Enabled {\n 634→\t\treturn\n 635→\t}\n 636→\tlog.Info(\"--- Relay ---\")\n 637→\tlog.Infof(\" Exposed address: %s\", cfg.Relay.ExposedAddress)\n 638→\tlog.Infof(\" Auth secret: %s...\", maskSecret(cfg.Relay.AuthSecret))\n 639→\tif cfg.Relay.Stun.Enabled {\n 640→\t\tlog.Infof(\" STUN ports: %v (log level: %s)\", cfg.Relay.Stun.Ports, cfg.Relay.Stun.LogLevel)\n 641→\t} else {\n 642→\t\tlog.Info(\" STUN: disabled\")\n 643→\t}\n 644→}\n 645→\n 646→func logManagementConfig(cfg *CombinedConfig) {\n 647→\tif !cfg.Management.Enabled {\n 648→\t\treturn\n 649→\t}\n 650→\tlog.Info(\"--- Management ---\")\n 651→\tlog.Infof(\" Data dir: %s\", cfg.Management.DataDir)\n 652→\tlog.Infof(\" DNS domain: %s\", cfg.Management.DnsDomain)\n 653→\tlog.Infof(\" Store engine: %s\", cfg.Management.Store.Engine)\n 654→\tif cfg.Server.Store.DSN != \"\" {\n 655→\t\tlog.Infof(\" Store DSN: %s\", maskDSNPassword(cfg.Server.Store.DSN))\n 656→\t}\n 657→\n 658→\tlog.Info(\" Auth (embedded IdP):\")\n 659→\tlog.Infof(\" Issuer: %s\", cfg.Management.Auth.Issuer)\n 660→\tlog.Infof(\" Dashboard redirect URIs: %v\", cfg.Management.Auth.DashboardRedirectURIs)\n 661→\tlog.Infof(\" CLI redirect URIs: %v\", cfg.Management.Auth.CLIRedirectURIs)\n 662→\n 663→\tlog.Info(\" Client settings:\")\n 664→\tlog.Infof(\" Signal URI: %s\", cfg.Management.SignalURI)\n 665→\tfor _, s := range cfg.Management.Stuns {\n 666→\t\tlog.Infof(\" STUN: %s\", s.URI)\n 667→\t}\n 668→\tif len(cfg.Management.Relays.Addresses) > 0 {\n 669→\t\tlog.Infof(\" Relay addresses: %v\", cfg.Management.Relays.Addresses)\n 670→\t\tlog.Infof(\" Relay credentials TTL: %s\", cfg.Management.Relays.CredentialsTTL)\n 671→\t}\n 672→}\n 673→\n 674→// logEnvVars logs all NB_ environment variables that are currently set\n 675→func logEnvVars() {\n 676→\tlog.Info(\"=== Environment Variables ===\")\n 677→\tfound := false\n 678→\tfor _, env := range os.Environ() {\n 679→\t\tif strings.HasPrefix(env, \"NB_\") {\n 680→\t\t\tkey, _, _ := strings.Cut(env, \"=\")\n 681→\t\t\tvalue := os.Getenv(key)\n 682→\t\t\tkeyLower := strings.ToLower(key)\n 683→\t\t\tif strings.Contains(keyLower, \"secret\") || strings.Contains(keyLower, \"key\") || strings.Contains(keyLower, \"password\") {\n 684→\t\t\t\tvalue = maskSecret(value)\n 685→\t\t\t} else if strings.Contains(keyLower, \"dsn\") {\n 686→\t\t\t\tvalue = maskDSNPassword(value)\n 687→\t\t\t}\n 688→\t\t\tlog.Infof(\" %s=%s\", key, value)\n 689→\t\t\tfound = true\n 690→\t\t}\n 691→\t}\n 692→\tif !found {\n 693→\t\tlog.Info(\" (none set)\")\n 694→\t}\n 695→\tlog.Info(\"=== End Environment Variables ===\")\n 696→}\n 697→\n 698→// maskDSNPassword masks the password in a DSN string.\n 699→// Handles both key=value format (\"password=secret\") and URI format (\"user:secret@host\").\n 700→func maskDSNPassword(dsn string) string {\n 701→\t// Key=value format: \"host=localhost user=nb password=secret dbname=nb\"\n 702→\tif strings.Contains(dsn, \"password=\") {\n 703→\t\tparts := strings.Fields(dsn)\n 704→\t\tfor i, p := range parts {\n 705→\t\t\tif strings.HasPrefix(p, \"password=\") {\n 706→\t\t\t\tparts[i] = \"password=****\"\n 707→\t\t\t}\n 708→\t\t}\n 709→\t\treturn strings.Join(parts, \" \")\n 710→\t}\n 711→\n 712→\t// URI format: \"user:password@host...\"\n 713→\tif atIdx := strings.Index(dsn, \"@\"); atIdx != -1 {\n 714→\t\tprefix := dsn[:atIdx]\n 715→\t\tif colonIdx := strings.Index(prefix, \":\"); colonIdx != -1 {\n 716→\t\t\treturn prefix[:colonIdx+1] + \"****\" + dsn[atIdx:]\n 717→\t\t}\n 718→\t}\n 719→\n 720→\treturn dsn\n 721→}\n 722→\n 723→// maskSecret returns first 4 chars of secret followed by \"...\"\n 724→func maskSecret(secret string) string {\n 725→\tif len(secret) <= 4 {\n 726→\t\treturn \"****\"\n 727→\t}\n 728→\treturn secret[:4] + \"...\"\n 729→}\n 730→"}]},"uuid":"997b008b-cf45-4ca8-8dc0-d482f1f7da8d","timestamp":"2026-03-02T18:56:37.013Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"8a667556-14e8-48ed-8c24-c60858e40247","timestamp":"2026-03-02T18:56:37.232Z"}
+{"parentUuid":"8a667556-14e8-48ed-8c24-c60858e40247","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01DZhxRFoNEu3fZL8kaTo1Y8","type":"tool_result","content":" 1→package cmd\n 2→\n 3→import (\n 4→\t\"context\"\n 5→\t\"fmt\"\n 6→\t\"net\"\n 7→\t\"net/netip\"\n 8→\t\"os\"\n 9→\t\"path\"\n 10→\t\"strings\"\n 11→\t\"time\"\n 12→\n 13→\tlog \"github.com/sirupsen/logrus\"\n 14→\t\"gopkg.in/yaml.v3\"\n 15→\n 16→\t\"github.com/netbirdio/netbird/management/server/idp\"\n 17→\t\"github.com/netbirdio/netbird/management/server/types\"\n 18→\t\"github.com/netbirdio/netbird/util\"\n 19→\t\"github.com/netbirdio/netbird/util/crypt\"\n 20→\n 21→\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n 22→)\n 23→\n 24→// CombinedConfig is the root configuration for the combined server.\n 25→// The combined server is primarily a Management server with optional embedded\n 26→// Signal, Relay, and STUN services.\n 27→//\n 28→// Architecture:\n 29→// - Management: Always runs locally (this IS the management server)\n 30→// - Signal: Runs locally by default; disabled if server.signalUri is set\n 31→// - Relay: Runs locally by default; disabled if server.relays is set\n 32→// - STUN: Runs locally on port 3478 by default; disabled if server.stuns is set\n 33→//\n 34→// All user-facing settings are under \"server\". The relay/signal/management\n 35→// fields are internal and populated automatically from server settings.\n 36→type CombinedConfig struct {\n 37→\tServer ServerConfig `yaml:\"server\"`\n 38→\n 39→\t// Internal configs - populated from Server settings, not user-configurable\n 40→\tRelay RelayConfig `yaml:\"-\"`\n 41→\tSignal SignalConfig `yaml:\"-\"`\n 42→\tManagement ManagementConfig `yaml:\"-\"`\n 43→}\n 44→\n 45→// ServerConfig contains server-wide settings\n 46→// In simplified mode, this contains all configuration\n 47→type ServerConfig struct {\n 48→\tListenAddress string `yaml:\"listenAddress\"`\n 49→\tMetricsPort int `yaml:\"metricsPort\"`\n 50→\tHealthcheckAddress string `yaml:\"healthcheckAddress\"`\n 51→\tLogLevel string `yaml:\"logLevel\"`\n 52→\tLogFile string `yaml:\"logFile\"`\n 53→\tTLS TLSConfig `yaml:\"tls\"`\n 54→\n 55→\t// Simplified config fields (used when relay/signal/management sections are omitted)\n 56→\tExposedAddress string `yaml:\"exposedAddress\"` // Public address with protocol (e.g., \"https://example.com:443\")\n 57→\tStunPorts []int `yaml:\"stunPorts\"` // STUN ports (empty to disable local STUN)\n 58→\tAuthSecret string `yaml:\"authSecret\"` // Shared secret for relay authentication\n 59→\tDataDir string `yaml:\"dataDir\"` // Data directory for all services\n 60→\n 61→\t// External service overrides (simplified mode)\n 62→\t// When these are set, the corresponding local service is NOT started\n 63→\t// and these values are used for client configuration instead\n 64→\tStuns []HostConfig `yaml:\"stuns\"` // External STUN servers (disables local STUN)\n 65→\tRelays RelaysConfig `yaml:\"relays\"` // External relay servers (disables local relay)\n 66→\tSignalURI string `yaml:\"signalUri\"` // External signal server (disables local signal)\n 67→\n 68→\t// Management settings (simplified mode)\n 69→\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n 70→\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n 71→\tAuth AuthConfig `yaml:\"auth\"`\n 72→\tStore StoreConfig `yaml:\"store\"`\n 73→\tActivityStore StoreConfig `yaml:\"activityStore\"`\n 74→\tAuthStore StoreConfig `yaml:\"authStore\"`\n 75→\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n 76→}\n 77→\n 78→// TLSConfig contains TLS/HTTPS settings\n 79→type TLSConfig struct {\n 80→\tCertFile string `yaml:\"certFile\"`\n 81→\tKeyFile string `yaml:\"keyFile\"`\n 82→\tLetsEncrypt LetsEncryptConfig `yaml:\"letsencrypt\"`\n 83→}\n 84→\n 85→// LetsEncryptConfig contains Let's Encrypt settings\n 86→type LetsEncryptConfig struct {\n 87→\tEnabled bool `yaml:\"enabled\"`\n 88→\tDataDir string `yaml:\"dataDir\"`\n 89→\tDomains []string `yaml:\"domains\"`\n 90→\tEmail string `yaml:\"email\"`\n 91→\tAWSRoute53 bool `yaml:\"awsRoute53\"`\n 92→}\n 93→\n 94→// RelayConfig contains relay service settings\n 95→type RelayConfig struct {\n 96→\tEnabled bool `yaml:\"enabled\"`\n 97→\tExposedAddress string `yaml:\"exposedAddress\"`\n 98→\tAuthSecret string `yaml:\"authSecret\"`\n 99→\tLogLevel string `yaml:\"logLevel\"`\n 100→\tStun StunConfig `yaml:\"stun\"`\n 101→}\n 102→\n 103→// StunConfig contains embedded STUN service settings\n 104→type StunConfig struct {\n 105→\tEnabled bool `yaml:\"enabled\"`\n 106→\tPorts []int `yaml:\"ports\"`\n 107→\tLogLevel string `yaml:\"logLevel\"`\n 108→}\n 109→\n 110→// SignalConfig contains signal service settings\n 111→type SignalConfig struct {\n 112→\tEnabled bool `yaml:\"enabled\"`\n 113→\tLogLevel string `yaml:\"logLevel\"`\n 114→}\n 115→\n 116→// ManagementConfig contains management service settings\n 117→type ManagementConfig struct {\n 118→\tEnabled bool `yaml:\"enabled\"`\n 119→\tLogLevel string `yaml:\"logLevel\"`\n 120→\tDataDir string `yaml:\"dataDir\"`\n 121→\tDnsDomain string `yaml:\"dnsDomain\"`\n 122→\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n 123→\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n 124→\tDisableDefaultPolicy bool `yaml:\"disableDefaultPolicy\"`\n 125→\tAuth AuthConfig `yaml:\"auth\"`\n 126→\tStuns []HostConfig `yaml:\"stuns\"`\n 127→\tRelays RelaysConfig `yaml:\"relays\"`\n 128→\tSignalURI string `yaml:\"signalUri\"`\n 129→\tStore StoreConfig `yaml:\"store\"`\n 130→\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n 131→}\n 132→\n 133→// AuthConfig contains authentication/identity provider settings\n 134→type AuthConfig struct {\n 135→\tIssuer string `yaml:\"issuer\"`\n 136→\tLocalAuthDisabled bool `yaml:\"localAuthDisabled\"`\n 137→\tSignKeyRefreshEnabled bool `yaml:\"signKeyRefreshEnabled\"`\n 138→\tStorage AuthStorageConfig `yaml:\"storage\"`\n 139→\tDashboardRedirectURIs []string `yaml:\"dashboardRedirectURIs\"`\n 140→\tCLIRedirectURIs []string `yaml:\"cliRedirectURIs\"`\n 141→\tOwner *AuthOwnerConfig `yaml:\"owner,omitempty\"`\n 142→}\n 143→\n 144→// AuthStorageConfig contains auth storage settings\n 145→type AuthStorageConfig struct {\n 146→\tType string `yaml:\"type\"`\n 147→\tFile string `yaml:\"file\"`\n 148→}\n 149→\n 150→// AuthOwnerConfig contains initial admin user settings\n 151→type AuthOwnerConfig struct {\n 152→\tEmail string `yaml:\"email\"`\n 153→\tPassword string `yaml:\"password\"`\n 154→}\n 155→\n 156→// HostConfig represents a STUN/TURN/Signal host\n 157→type HostConfig struct {\n 158→\tURI string `yaml:\"uri\"`\n 159→\tProto string `yaml:\"proto,omitempty\"` // udp, dtls, tcp, http, https - defaults based on URI scheme\n 160→\tUsername string `yaml:\"username,omitempty\"`\n 161→\tPassword string `yaml:\"password,omitempty\"`\n 162→}\n 163→\n 164→// RelaysConfig contains external relay server settings for clients\n 165→type RelaysConfig struct {\n 166→\tAddresses []string `yaml:\"addresses\"`\n 167→\tCredentialsTTL string `yaml:\"credentialsTTL\"`\n 168→\tSecret string `yaml:\"secret\"`\n 169→}\n 170→\n 171→// StoreConfig contains database settings\n 172→type StoreConfig struct {\n 173→\tEngine string `yaml:\"engine\"`\n 174→\tEncryptionKey string `yaml:\"encryptionKey\"`\n 175→\tDSN string `yaml:\"dsn\"` // Connection string for postgres or mysql engines\n 176→}\n 177→\n 178→// ReverseProxyConfig contains reverse proxy settings\n 179→type ReverseProxyConfig struct {\n 180→\tTrustedHTTPProxies []string `yaml:\"trustedHTTPProxies\"`\n 181→\tTrustedHTTPProxiesCount uint `yaml:\"trustedHTTPProxiesCount\"`\n 182→\tTrustedPeers []string `yaml:\"trustedPeers\"`\n 183→}\n 184→\n 185→// DefaultConfig returns a CombinedConfig with default values\n 186→func DefaultConfig() *CombinedConfig {\n 187→\treturn &CombinedConfig{\n 188→\t\tServer: ServerConfig{\n 189→\t\t\tListenAddress: \":443\",\n 190→\t\t\tMetricsPort: 9090,\n 191→\t\t\tHealthcheckAddress: \":9000\",\n 192→\t\t\tLogLevel: \"info\",\n 193→\t\t\tLogFile: \"console\",\n 194→\t\t\tStunPorts: []int{3478},\n 195→\t\t\tDataDir: \"/var/lib/netbird/\",\n 196→\t\t\tAuth: AuthConfig{\n 197→\t\t\t\tStorage: AuthStorageConfig{\n 198→\t\t\t\t\tType: \"sqlite3\",\n 199→\t\t\t\t},\n 200→\t\t\t},\n 201→\t\t\tStore: StoreConfig{\n 202→\t\t\t\tEngine: \"sqlite\",\n 203→\t\t\t},\n 204→\t\t},\n 205→\t\tRelay: RelayConfig{\n 206→\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n 207→\t\t\tStun: StunConfig{\n 208→\t\t\t\tEnabled: false,\n 209→\t\t\t\tPorts: []int{3478},\n 210→\t\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n 211→\t\t\t},\n 212→\t\t},\n 213→\t\tSignal: SignalConfig{\n 214→\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n 215→\t\t},\n 216→\t\tManagement: ManagementConfig{\n 217→\t\t\tDataDir: \"/var/lib/netbird/\",\n 218→\t\t\tAuth: AuthConfig{\n 219→\t\t\t\tStorage: AuthStorageConfig{\n 220→\t\t\t\t\tType: \"sqlite3\",\n 221→\t\t\t\t},\n 222→\t\t\t},\n 223→\t\t\tRelays: RelaysConfig{\n 224→\t\t\t\tCredentialsTTL: \"12h\",\n 225→\t\t\t},\n 226→\t\t\tStore: StoreConfig{\n 227→\t\t\t\tEngine: \"sqlite\",\n 228→\t\t\t},\n 229→\t\t},\n 230→\t}\n 231→}\n 232→\n 233→// hasRequiredSettings returns true if the configuration has the required server settings\n 234→func (c *CombinedConfig) hasRequiredSettings() bool {\n 235→\treturn c.Server.ExposedAddress != \"\"\n 236→}\n 237→\n 238→// parseExposedAddress extracts protocol, host, and host:port from the exposed address\n 239→// Input format: \"https://example.com:443\" or \"http://example.com:8080\" or \"example.com:443\"\n 240→// Returns: protocol (\"https\" or \"http\"), hostname only, and host:port\n 241→func parseExposedAddress(exposedAddress string) (protocol, hostname, hostPort string) {\n 242→\t// Default to https if no protocol specified\n 243→\tprotocol = \"https\"\n 244→\thostPort = exposedAddress\n 245→\n 246→\t// Check for protocol prefix\n 247→\tif strings.HasPrefix(exposedAddress, \"https://\") {\n 248→\t\tprotocol = \"https\"\n 249→\t\thostPort = strings.TrimPrefix(exposedAddress, \"https://\")\n 250→\t} else if strings.HasPrefix(exposedAddress, \"http://\") {\n 251→\t\tprotocol = \"http\"\n 252→\t\thostPort = strings.TrimPrefix(exposedAddress, \"http://\")\n 253→\t}\n 254→\n 255→\t// Extract hostname (without port)\n 256→\thostname = hostPort\n 257→\tif host, _, err := net.SplitHostPort(hostPort); err == nil {\n 258→\t\thostname = host\n 259→\t}\n 260→\n 261→\treturn protocol, hostname, hostPort\n 262→}\n 263→\n 264→// ApplySimplifiedDefaults populates internal relay/signal/management configs from server settings.\n 265→// Management is always enabled. Signal, Relay, and STUN are enabled unless external\n 266→// overrides are configured (server.signalUri, server.relays, server.stuns).\n 267→func (c *CombinedConfig) ApplySimplifiedDefaults() {\n 268→\tif !c.hasRequiredSettings() {\n 269→\t\treturn\n 270→\t}\n 271→\n 272→\t// Parse exposed address to extract protocol and hostname\n 273→\texposedProto, exposedHost, exposedHostPort := parseExposedAddress(c.Server.ExposedAddress)\n 274→\n 275→\t// Check for external service overrides\n 276→\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n 277→\thasExternalSignal := c.Server.SignalURI != \"\"\n 278→\thasExternalStuns := len(c.Server.Stuns) > 0\n 279→\n 280→\t// Default stunPorts to [3478] if not specified and no external STUN\n 281→\tif len(c.Server.StunPorts) == 0 && !hasExternalStuns {\n 282→\t\tc.Server.StunPorts = []int{3478}\n 283→\t}\n 284→\n 285→\tc.applyRelayDefaults(exposedProto, exposedHostPort, hasExternalRelay, hasExternalStuns)\n 286→\tc.applySignalDefaults(hasExternalSignal)\n 287→\tc.applyManagementDefaults(exposedHost)\n 288→\n 289→\t// Auto-configure client settings (stuns, relays, signalUri)\n 290→\tc.autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort, hasExternalStuns, hasExternalRelay, hasExternalSignal)\n 291→}\n 292→\n 293→// applyRelayDefaults configures the relay service if no external relay is configured.\n 294→func (c *CombinedConfig) applyRelayDefaults(exposedProto, exposedHostPort string, hasExternalRelay, hasExternalStuns bool) {\n 295→\tif hasExternalRelay {\n 296→\t\treturn\n 297→\t}\n 298→\n 299→\tc.Relay.Enabled = true\n 300→\trelayProto := \"rel\"\n 301→\tif exposedProto == \"https\" {\n 302→\t\trelayProto = \"rels\"\n 303→\t}\n 304→\tc.Relay.ExposedAddress = fmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort)\n 305→\tc.Relay.AuthSecret = c.Server.AuthSecret\n 306→\tif c.Relay.LogLevel == \"\" {\n 307→\t\tc.Relay.LogLevel = c.Server.LogLevel\n 308→\t}\n 309→\n 310→\t// Enable local STUN only if no external STUN servers and stunPorts are configured\n 311→\tif !hasExternalStuns && len(c.Server.StunPorts) > 0 {\n 312→\t\tc.Relay.Stun.Enabled = true\n 313→\t\tc.Relay.Stun.Ports = c.Server.StunPorts\n 314→\t\tif c.Relay.Stun.LogLevel == \"\" {\n 315→\t\t\tc.Relay.Stun.LogLevel = c.Server.LogLevel\n 316→\t\t}\n 317→\t}\n 318→}\n 319→\n 320→// applySignalDefaults configures the signal service if no external signal is configured.\n 321→func (c *CombinedConfig) applySignalDefaults(hasExternalSignal bool) {\n 322→\tif hasExternalSignal {\n 323→\t\treturn\n 324→\t}\n 325→\n 326→\tc.Signal.Enabled = true\n 327→\tif c.Signal.LogLevel == \"\" {\n 328→\t\tc.Signal.LogLevel = c.Server.LogLevel\n 329→\t}\n 330→}\n 331→\n 332→// applyManagementDefaults configures the management service (always enabled).\n 333→func (c *CombinedConfig) applyManagementDefaults(exposedHost string) {\n 334→\tc.Management.Enabled = true\n 335→\tif c.Management.LogLevel == \"\" {\n 336→\t\tc.Management.LogLevel = c.Server.LogLevel\n 337→\t}\n 338→\tif c.Management.DataDir == \"\" || c.Management.DataDir == \"/var/lib/netbird/\" {\n 339→\t\tc.Management.DataDir = c.Server.DataDir\n 340→\t}\n 341→\tc.Management.DnsDomain = exposedHost\n 342→\tc.Management.DisableAnonymousMetrics = c.Server.DisableAnonymousMetrics\n 343→\tc.Management.DisableGeoliteUpdate = c.Server.DisableGeoliteUpdate\n 344→\t// Copy auth config from server if management auth issuer is not set\n 345→\tif c.Management.Auth.Issuer == \"\" && c.Server.Auth.Issuer != \"\" {\n 346→\t\tc.Management.Auth = c.Server.Auth\n 347→\t}\n 348→\n 349→\t// Copy store config from server if not set\n 350→\tif c.Management.Store.Engine == \"\" || c.Management.Store.Engine == \"sqlite\" {\n 351→\t\tif c.Server.Store.Engine != \"\" {\n 352→\t\t\tc.Management.Store = c.Server.Store\n 353→\t\t}\n 354→\t}\n 355→\n 356→\t// Copy reverse proxy config from server\n 357→\tif len(c.Server.ReverseProxy.TrustedHTTPProxies) > 0 || c.Server.ReverseProxy.TrustedHTTPProxiesCount > 0 || len(c.Server.ReverseProxy.TrustedPeers) > 0 {\n 358→\t\tc.Management.ReverseProxy = c.Server.ReverseProxy\n 359→\t}\n 360→}\n 361→\n 362→// autoConfigureClientSettings sets up STUN/relay/signal URIs for clients\n 363→// External overrides from server config take precedence over auto-generated values\n 364→func (c *CombinedConfig) autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort string, hasExternalStuns, hasExternalRelay, hasExternalSignal bool) {\n 365→\t// Determine relay protocol from exposed protocol\n 366→\trelayProto := \"rel\"\n 367→\tif exposedProto == \"https\" {\n 368→\t\trelayProto = \"rels\"\n 369→\t}\n 370→\n 371→\t// Configure STUN servers for clients\n 372→\tif hasExternalStuns {\n 373→\t\t// Use external STUN servers from server config\n 374→\t\tc.Management.Stuns = c.Server.Stuns\n 375→\t} else if len(c.Server.StunPorts) > 0 && len(c.Management.Stuns) == 0 {\n 376→\t\t// Auto-configure local STUN servers for all ports\n 377→\t\tfor _, port := range c.Server.StunPorts {\n 378→\t\t\tc.Management.Stuns = append(c.Management.Stuns, HostConfig{\n 379→\t\t\t\tURI: fmt.Sprintf(\"stun:%s:%d\", exposedHost, port),\n 380→\t\t\t})\n 381→\t\t}\n 382→\t}\n 383→\n 384→\t// Configure relay for clients\n 385→\tif hasExternalRelay {\n 386→\t\t// Use external relay config from server\n 387→\t\tc.Management.Relays = c.Server.Relays\n 388→\t} else if len(c.Management.Relays.Addresses) == 0 {\n 389→\t\t// Auto-configure local relay\n 390→\t\tc.Management.Relays.Addresses = []string{\n 391→\t\t\tfmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort),\n 392→\t\t}\n 393→\t}\n 394→\tif c.Management.Relays.Secret == \"\" {\n 395→\t\tc.Management.Relays.Secret = c.Server.AuthSecret\n 396→\t}\n 397→\tif c.Management.Relays.CredentialsTTL == \"\" {\n 398→\t\tc.Management.Relays.CredentialsTTL = \"12h\"\n 399→\t}\n 400→\n 401→\t// Configure signal for clients\n 402→\tif hasExternalSignal {\n 403→\t\t// Use external signal URI from server config\n 404→\t\tc.Management.SignalURI = c.Server.SignalURI\n 405→\t} else if c.Management.SignalURI == \"\" {\n 406→\t\t// Auto-configure local signal\n 407→\t\tc.Management.SignalURI = fmt.Sprintf(\"%s://%s\", exposedProto, exposedHostPort)\n 408→\t}\n 409→}\n 410→\n 411→// LoadConfig loads configuration from a YAML file\n 412→func LoadConfig(configPath string) (*CombinedConfig, error) {\n 413→\tcfg := DefaultConfig()\n 414→\n 415→\tif configPath == \"\" {\n 416→\t\treturn cfg, nil\n 417→\t}\n 418→\n 419→\tdata, err := os.ReadFile(configPath)\n 420→\tif err != nil {\n 421→\t\treturn nil, fmt.Errorf(\"failed to read config file: %w\", err)\n 422→\t}\n 423→\n 424→\tif err := yaml.Unmarshal(data, cfg); err != nil {\n 425→\t\treturn nil, fmt.Errorf(\"failed to parse config file: %w\", err)\n 426→\t}\n 427→\n 428→\t// Populate internal configs from server settings\n 429→\tcfg.ApplySimplifiedDefaults()\n 430→\n 431→\treturn cfg, nil\n 432→}\n 433→\n 434→// Validate validates the configuration\n 435→func (c *CombinedConfig) Validate() error {\n 436→\tif c.Server.ExposedAddress == \"\" {\n 437→\t\treturn fmt.Errorf(\"server.exposedAddress is required\")\n 438→\t}\n 439→\tif c.Server.DataDir == \"\" {\n 440→\t\treturn fmt.Errorf(\"server.dataDir is required\")\n 441→\t}\n 442→\n 443→\t// Validate STUN ports\n 444→\tseen := make(map[int]bool)\n 445→\tfor _, port := range c.Server.StunPorts {\n 446→\t\tif port <= 0 || port > 65535 {\n 447→\t\t\treturn fmt.Errorf(\"invalid server.stunPorts value %d: must be between 1 and 65535\", port)\n 448→\t\t}\n 449→\t\tif seen[port] {\n 450→\t\t\treturn fmt.Errorf(\"duplicate STUN port %d in server.stunPorts\", port)\n 451→\t\t}\n 452→\t\tseen[port] = true\n 453→\t}\n 454→\n 455→\t// authSecret is required only if running local relay (no external relay configured)\n 456→\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n 457→\tif !hasExternalRelay && c.Server.AuthSecret == \"\" {\n 458→\t\treturn fmt.Errorf(\"server.authSecret is required when running local relay\")\n 459→\t}\n 460→\n 461→\treturn nil\n 462→}\n 463→\n 464→// HasTLSCert returns true if TLS certificate files are configured\n 465→func (c *CombinedConfig) HasTLSCert() bool {\n 466→\treturn c.Server.TLS.CertFile != \"\" && c.Server.TLS.KeyFile != \"\"\n 467→}\n 468→\n 469→// HasLetsEncrypt returns true if Let's Encrypt is configured\n 470→func (c *CombinedConfig) HasLetsEncrypt() bool {\n 471→\treturn c.Server.TLS.LetsEncrypt.Enabled &&\n 472→\t\tc.Server.TLS.LetsEncrypt.DataDir != \"\" &&\n 473→\t\tlen(c.Server.TLS.LetsEncrypt.Domains) > 0\n 474→}\n 475→\n 476→// parseExplicitProtocol parses an explicit protocol string to nbconfig.Protocol\n 477→func parseExplicitProtocol(proto string) (nbconfig.Protocol, bool) {\n 478→\tswitch strings.ToLower(proto) {\n 479→\tcase \"udp\":\n 480→\t\treturn nbconfig.UDP, true\n 481→\tcase \"dtls\":\n 482→\t\treturn nbconfig.DTLS, true\n 483→\tcase \"tcp\":\n 484→\t\treturn nbconfig.TCP, true\n 485→\tcase \"http\":\n 486→\t\treturn nbconfig.HTTP, true\n 487→\tcase \"https\":\n 488→\t\treturn nbconfig.HTTPS, true\n 489→\tdefault:\n 490→\t\treturn \"\", false\n 491→\t}\n 492→}\n 493→\n 494→// parseStunProtocol determines protocol for STUN/TURN servers.\n 495→// stun: → UDP, stuns: → DTLS, turn: → UDP, turns: → DTLS\n 496→// Explicit proto overrides URI scheme. Defaults to UDP.\n 497→func parseStunProtocol(uri, proto string) nbconfig.Protocol {\n 498→\tif proto != \"\" {\n 499→\t\tif p, ok := parseExplicitProtocol(proto); ok {\n 500→\t\t\treturn p\n 501→\t\t}\n 502→\t}\n 503→\n 504→\turi = strings.ToLower(uri)\n 505→\tswitch {\n 506→\tcase strings.HasPrefix(uri, \"stuns:\"):\n 507→\t\treturn nbconfig.DTLS\n 508→\tcase strings.HasPrefix(uri, \"turns:\"):\n 509→\t\treturn nbconfig.DTLS\n 510→\tdefault:\n 511→\t\t// stun:, turn:, or no scheme - default to UDP\n 512→\t\treturn nbconfig.UDP\n 513→\t}\n 514→}\n 515→\n 516→// parseSignalProtocol determines protocol for Signal servers.\n 517→// https:// → HTTPS, http:// → HTTP. Defaults to HTTPS.\n 518→func parseSignalProtocol(uri string) nbconfig.Protocol {\n 519→\turi = strings.ToLower(uri)\n 520→\tswitch {\n 521→\tcase strings.HasPrefix(uri, \"http://\"):\n 522→\t\treturn nbconfig.HTTP\n 523→\tdefault:\n 524→\t\t// https:// or no scheme - default to HTTPS\n 525→\t\treturn nbconfig.HTTPS\n 526→\t}\n 527→}\n 528→\n 529→// stripSignalProtocol removes the protocol prefix from a signal URI.\n 530→// Returns just the host:port (e.g., \"selfhosted2.demo.netbird.io:443\").\n 531→func stripSignalProtocol(uri string) string {\n 532→\turi = strings.TrimPrefix(uri, \"https://\")\n 533→\turi = strings.TrimPrefix(uri, \"http://\")\n 534→\treturn uri\n 535→}\n 536→\n 537→func buildRelayConfig(relays RelaysConfig) (*nbconfig.Relay, error) {\n 538→\tvar ttl time.Duration\n 539→\tif relays.CredentialsTTL != \"\" {\n 540→\t\tvar err error\n 541→\t\tttl, err = time.ParseDuration(relays.CredentialsTTL)\n 542→\t\tif err != nil {\n 543→\t\t\treturn nil, fmt.Errorf(\"invalid relay credentials TTL %q: %w\", relays.CredentialsTTL, err)\n 544→\t\t}\n 545→\t}\n 546→\treturn &nbconfig.Relay{\n 547→\t\tAddresses: relays.Addresses,\n 548→\t\tCredentialsTTL: util.Duration{Duration: ttl},\n 549→\t\tSecret: relays.Secret,\n 550→\t}, nil\n 551→}\n 552→\n 553→// buildEmbeddedIdPConfig builds the embedded IdP configuration.\n 554→// authStore overrides auth.storage when set.\n 555→func (c *CombinedConfig) buildEmbeddedIdPConfig(mgmt ManagementConfig) (*idp.EmbeddedIdPConfig, error) {\n 556→\tauthStorageType := mgmt.Auth.Storage.Type\n 557→\tauthStorageDSN := c.Server.AuthStore.DSN\n 558→\tif c.Server.AuthStore.Engine != \"\" {\n 559→\t\tauthStorageType = c.Server.AuthStore.Engine\n 560→\t}\n 561→\tif authStorageType == \"\" {\n 562→\t\tauthStorageType = \"sqlite3\"\n 563→\t}\n 564→\tauthStorageFile := \"\"\n 565→\tif authStorageType == \"postgres\" {\n 566→\t\tif authStorageDSN == \"\" {\n 567→\t\t\treturn nil, fmt.Errorf(\"authStore.dsn is required when authStore.engine is postgres\")\n 568→\t\t}\n 569→\t} else {\n 570→\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n 571→\t}\n 572→\n 573→\tcfg := &idp.EmbeddedIdPConfig{\n 574→\t\tEnabled: true,\n 575→\t\tIssuer: mgmt.Auth.Issuer,\n 576→\t\tLocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,\n 577→\t\tSignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,\n 578→\t\tStorage: idp.EmbeddedStorageConfig{\n 579→\t\t\tType: authStorageType,\n 580→\t\t\tConfig: idp.EmbeddedStorageTypeConfig{\n 581→\t\t\t\tFile: authStorageFile,\n 582→\t\t\t\tDSN: authStorageDSN,\n 583→\t\t\t},\n 584→\t\t},\n 585→\t\tDashboardRedirectURIs: mgmt.Auth.DashboardRedirectURIs,\n 586→\t\tCLIRedirectURIs: mgmt.Auth.CLIRedirectURIs,\n 587→\t}\n 588→\n 589→\tif mgmt.Auth.Owner != nil && mgmt.Auth.Owner.Email != \"\" {\n 590→\t\tcfg.Owner = &idp.OwnerConfig{\n 591→\t\t\tEmail: mgmt.Auth.Owner.Email,\n 592→\t\t\tHash: mgmt.Auth.Owner.Password,\n 593→\t\t}\n 594→\t}\n 595→\n 596→\treturn cfg, nil\n 597→}\n 598→\n 599→// ToManagementConfig converts CombinedConfig to management server config\n 600→func (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {\n 601→\tmgmt := c.Management\n 602→\n 603→\t// Build STUN hosts\n 604→\tvar stuns []*nbconfig.Host\n 605→\tfor _, s := range mgmt.Stuns {\n 606→\t\tstuns = append(stuns, &nbconfig.Host{\n 607→\t\t\tURI: s.URI,\n 608→\t\t\tProto: parseStunProtocol(s.URI, s.Proto),\n 609→\t\t\tUsername: s.Username,\n 610→\t\t\tPassword: s.Password,\n 611→\t\t})\n 612→\t}\n 613→\n 614→\t// Build relay config\n 615→\tvar relayConfig *nbconfig.Relay\n 616→\tif len(mgmt.Relays.Addresses) > 0 || mgmt.Relays.Secret != \"\" {\n 617→\t\trelay, err := buildRelayConfig(mgmt.Relays)\n 618→\t\tif err != nil {\n 619→\t\t\treturn nil, err\n 620→\t\t}\n 621→\t\trelayConfig = relay\n 622→\t}\n 623→\n 624→\t// Build signal config\n 625→\tvar signalConfig *nbconfig.Host\n 626→\tif mgmt.SignalURI != \"\" {\n 627→\t\tsignalConfig = &nbconfig.Host{\n 628→\t\t\tURI: stripSignalProtocol(mgmt.SignalURI),\n 629→\t\t\tProto: parseSignalProtocol(mgmt.SignalURI),\n 630→\t\t}\n 631→\t}\n 632→\n 633→\t// Build store config\n 634→\tstoreConfig := nbconfig.StoreConfig{\n 635→\t\tEngine: types.Engine(mgmt.Store.Engine),\n 636→\t}\n 637→\n 638→\t// Build reverse proxy config\n 639→\treverseProxy := nbconfig.ReverseProxy{\n 640→\t\tTrustedHTTPProxiesCount: mgmt.ReverseProxy.TrustedHTTPProxiesCount,\n 641→\t}\n 642→\tfor _, p := range mgmt.ReverseProxy.TrustedHTTPProxies {\n 643→\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n 644→\t\t\treverseProxy.TrustedHTTPProxies = append(reverseProxy.TrustedHTTPProxies, prefix)\n 645→\t\t}\n 646→\t}\n 647→\tfor _, p := range mgmt.ReverseProxy.TrustedPeers {\n 648→\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n 649→\t\t\treverseProxy.TrustedPeers = append(reverseProxy.TrustedPeers, prefix)\n 650→\t\t}\n 651→\t}\n 652→\n 653→\t// Build HTTP config (required, even if empty)\n 654→\thttpConfig := &nbconfig.HttpServerConfig{}\n 655→\n 656→\t// Build embedded IDP config (always enabled in combined server)\n 657→\tembeddedIdP, err := c.buildEmbeddedIdPConfig(mgmt)\n 658→\tif err != nil {\n 659→\t\treturn nil, err\n 660→\t}\n 661→\n 662→\t// Set HTTP config fields for embedded IDP\n 663→\thttpConfig.AuthIssuer = mgmt.Auth.Issuer\n 664→\thttpConfig.AuthAudience = \"netbird-dashboard\"\n 665→\thttpConfig.AuthClientID = httpConfig.AuthAudience\n 666→\thttpConfig.CLIAuthAudience = \"netbird-cli\"\n 667→\thttpConfig.AuthUserIDClaim = \"sub\"\n 668→\thttpConfig.AuthKeysLocation = mgmt.Auth.Issuer + \"/keys\"\n 669→\thttpConfig.OIDCConfigEndpoint = mgmt.Auth.Issuer + \"/.well-known/openid-configuration\"\n 670→\thttpConfig.IdpSignKeyRefreshEnabled = mgmt.Auth.SignKeyRefreshEnabled\n 671→\tcallbackURL := strings.TrimSuffix(httpConfig.AuthIssuer, \"/oauth2\")\n 672→\thttpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull\n 673→\n 674→\treturn &nbconfig.Config{\n 675→\t\tStuns: stuns,\n 676→\t\tRelay: relayConfig,\n 677→\t\tSignal: signalConfig,\n 678→\t\tDatadir: mgmt.DataDir,\n 679→\t\tDataStoreEncryptionKey: mgmt.Store.EncryptionKey,\n 680→\t\tHttpConfig: httpConfig,\n 681→\t\tStoreConfig: storeConfig,\n 682→\t\tReverseProxy: reverseProxy,\n 683→\t\tDisableDefaultPolicy: mgmt.DisableDefaultPolicy,\n 684→\t\tEmbeddedIdP: embeddedIdP,\n 685→\t}, nil\n 686→}\n 687→\n 688→// ApplyEmbeddedIdPConfig applies embedded IdP configuration to the management config.\n 689→// This mirrors the logic in management/cmd/management.go ApplyEmbeddedIdPConfig.\n 690→func ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config, mgmtPort int, disableSingleAccMode bool) error {\n 691→\tif cfg.EmbeddedIdP == nil || !cfg.EmbeddedIdP.Enabled {\n 692→\t\treturn nil\n 693→\t}\n 694→\n 695→\t// Embedded IdP requires single account mode\n 696→\tif disableSingleAccMode {\n 697→\t\treturn fmt.Errorf(\"embedded IdP requires single account mode; multiple account mode is not supported with embedded IdP\")\n 698→\t}\n 699→\n 700→\t// Set LocalAddress for embedded IdP, used for internal JWT validation\n 701→\tcfg.EmbeddedIdP.LocalAddress = fmt.Sprintf(\"localhost:%d\", mgmtPort)\n 702→\n 703→\t// Set storage defaults based on Datadir\n 704→\tif cfg.EmbeddedIdP.Storage.Type == \"\" {\n 705→\t\tcfg.EmbeddedIdP.Storage.Type = \"sqlite3\"\n 706→\t}\n 707→\tif cfg.EmbeddedIdP.Storage.Config.File == \"\" && cfg.Datadir != \"\" {\n 708→\t\tcfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")\n 709→\t}\n 710→\n 711→\tissuer := cfg.EmbeddedIdP.Issuer\n 712→\n 713→\t// Ensure HttpConfig exists\n 714→\tif cfg.HttpConfig == nil {\n 715→\t\tcfg.HttpConfig = &nbconfig.HttpServerConfig{}\n 716→\t}\n 717→\n 718→\t// Set HttpConfig values from EmbeddedIdP\n 719→\tcfg.HttpConfig.AuthIssuer = issuer\n 720→\tcfg.HttpConfig.AuthAudience = \"netbird-dashboard\"\n 721→\tcfg.HttpConfig.CLIAuthAudience = \"netbird-cli\"\n 722→\tcfg.HttpConfig.AuthUserIDClaim = \"sub\"\n 723→\tcfg.HttpConfig.AuthKeysLocation = issuer + \"/keys\"\n 724→\tcfg.HttpConfig.OIDCConfigEndpoint = issuer + \"/.well-known/openid-configuration\"\n 725→\tcfg.HttpConfig.IdpSignKeyRefreshEnabled = true\n 726→\n 727→\treturn nil\n 728→}\n 729→\n 730→// EnsureEncryptionKey generates an encryption key if not set.\n 731→// Unlike management server, we don't write back to the config file.\n 732→func EnsureEncryptionKey(ctx context.Context, cfg *nbconfig.Config) error {\n 733→\tif cfg.DataStoreEncryptionKey != \"\" {\n 734→\t\treturn nil\n 735→\t}\n 736→\n 737→\tlog.WithContext(ctx).Infof(\"DataStoreEncryptionKey is not set, generating a new key\")\n 738→\tkey, err := crypt.GenerateKey()\n 739→\tif err != nil {\n 740→\t\treturn fmt.Errorf(\"failed to generate datastore encryption key: %v\", err)\n 741→\t}\n 742→\tcfg.DataStoreEncryptionKey = key\n 743→\tkeyPreview := key[:8] + \"...\"\n 744→\tlog.WithContext(ctx).Warnf(\"DataStoreEncryptionKey generated (%s); add it to your config file under 'server.store.encryptionKey' to persist across restarts\", keyPreview)\n 745→\n 746→\treturn nil\n 747→}\n 748→\n 749→// LogConfigInfo logs informational messages about the loaded configuration\n 750→func LogConfigInfo(cfg *nbconfig.Config) {\n 751→\tif cfg.EmbeddedIdP != nil && cfg.EmbeddedIdP.Enabled {\n 752→\t\tlog.Infof(\"running with the embedded IdP: %v\", cfg.EmbeddedIdP.Issuer)\n 753→\t}\n 754→\tif cfg.Relay != nil {\n 755→\t\tlog.Infof(\"Relay addresses: %v\", cfg.Relay.Addresses)\n 756→\t}\n 757→}\n 758→"}]},"uuid":"350fbf9f-d85a-4f74-a213-49a502ba443d","timestamp":"2026-03-02T18:56:37.239Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"b7de7e98-0b16-419c-92d3-aead1af95418","timestamp":"2026-03-02T18:56:37.241Z"}
+{"parentUuid":"b7de7e98-0b16-419c-92d3-aead1af95418","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"assistant","timestamp":"2026-03-02T18:56:37.261Z","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01Ma5P3ckdgNnDW1NbvXrVT3","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01R1k6d3Kdszr25qiTS5RSNR","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/token.go"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":1344,"cache_read_input_tokens":11216,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":1344},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"requestId":"req_011CYemG5e4ZCWdrCetYtMR9","uuid":"dc8af912-65a6-485a-ac22-8c2c653bbbfc"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"4ee7368f-49c7-4dbe-a772-73e57e882cd8","timestamp":"2026-03-02T18:56:37.261Z"}
+{"parentUuid":"4ee7368f-49c7-4dbe-a772-73e57e882cd8","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01R1k6d3Kdszr25qiTS5RSNR","type":"tool_result","content":" 1→package cmd\n 2→\n 3→import (\n 4→\t\"context\"\n 5→\t\"fmt\"\n 6→\t\"os\"\n 7→\t\"strings\"\n 8→\n 9→\tlog \"github.com/sirupsen/logrus\"\n 10→\t\"github.com/spf13/cobra\"\n 11→\n 12→\t\"github.com/netbirdio/netbird/formatter/hook\"\n 13→\ttokencmd \"github.com/netbirdio/netbird/management/cmd/token\"\n 14→\t\"github.com/netbirdio/netbird/management/server/store\"\n 15→\t\"github.com/netbirdio/netbird/management/server/types\"\n 16→\t\"github.com/netbirdio/netbird/util\"\n 17→)\n 18→\n 19→// newTokenCommands creates the token command tree with combined-specific store opener.\n 20→func newTokenCommands() *cobra.Command {\n 21→\treturn tokencmd.NewCommands(withTokenStore)\n 22→}\n 23→\n 24→// withTokenStore loads the combined YAML config, initializes the store, and calls fn.\n 25→func withTokenStore(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error {\n 26→\tif err := util.InitLog(\"error\", \"console\"); err != nil {\n 27→\t\treturn fmt.Errorf(\"init log: %w\", err)\n 28→\t}\n 29→\n 30→\tctx := context.WithValue(cmd.Context(), hook.ExecutionContextKey, hook.SystemSource) //nolint:staticcheck\n 31→\n 32→\tcfg, err := LoadConfig(configPath)\n 33→\tif err != nil {\n 34→\t\treturn fmt.Errorf(\"load config: %w\", err)\n 35→\t}\n 36→\n 37→\tif dsn := cfg.Server.Store.DSN; dsn != \"\" {\n 38→\t\tswitch strings.ToLower(cfg.Server.Store.Engine) {\n 39→\t\tcase \"postgres\":\n 40→\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n 41→\t\tcase \"mysql\":\n 42→\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n 43→\t\t}\n 44→\t}\n 45→\n 46→\tdatadir := cfg.Management.DataDir\n 47→\tengine := types.Engine(cfg.Management.Store.Engine)\n 48→\n 49→\ts, err := store.NewStore(ctx, engine, datadir, nil, true)\n 50→\tif err != nil {\n 51→\t\treturn fmt.Errorf(\"create store: %w\", err)\n 52→\t}\n 53→\tdefer func() {\n 54→\t\tif err := s.Close(ctx); err != nil {\n 55→\t\t\tlog.Debugf(\"close store: %v\", err)\n 56→\t\t}\n 57→\t}()\n 58→\n 59→\treturn fn(ctx, s)\n 60→}\n 61→"}]},"uuid":"96244abc-4c92-4a86-ac0e-411c27ff0413","timestamp":"2026-03-02T18:56:37.276Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"c5190eab-8b66-406e-9ae4-b3e188b56f77","timestamp":"2026-03-02T18:56:37.500Z"}
+{"parentUuid":"c5190eab-8b66-406e-9ae4-b3e188b56f77","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"assistant","timestamp":"2026-03-02T18:56:37.544Z","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01Ma5P3ckdgNnDW1NbvXrVT3","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01AXv8tM4apYqA7dhtnkR9w8","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/config.yaml.example"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":1344,"cache_read_input_tokens":11216,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":1344},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"requestId":"req_011CYemG5e4ZCWdrCetYtMR9","uuid":"5caab279-41d3-4add-8f03-ccbbda87e9c4"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"9ac7fd71-7050-4dce-96f3-79f7ceacccc6","timestamp":"2026-03-02T18:56:37.544Z"}
+{"parentUuid":"9ac7fd71-7050-4dce-96f3-79f7ceacccc6","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01AXv8tM4apYqA7dhtnkR9w8","type":"tool_result","content":" 1→# NetBird Combined Server Configuration\n 2→# Copy this file to config.yaml and customize for your deployment\n 3→#\n 4→# This is a Management server with optional embedded Signal, Relay, and STUN services.\n 5→# By default, all services run locally. You can use external services instead by\n 6→# setting the corresponding override fields.\n 7→#\n 8→# Architecture:\n 9→# - Management: Always runs locally (this IS the management server)\n 10→# - Signal: Local by default; set 'signalUri' to use external (disables local)\n 11→# - Relay: Local by default; set 'relays' to use external (disables local)\n 12→# - STUN: Local on port 3478 by default; set 'stuns' to use external instead\n 13→\n 14→server:\n 15→ # Main HTTP/gRPC port for all services (Management, Signal, Relay)\n 16→ listenAddress: \":443\"\n 17→\n 18→ # Public address that peers will use to connect to this server\n 19→ # Used for relay connections and management DNS domain\n 20→ # Format: protocol://hostname:port (e.g., https://server.mycompany.com:443)\n 21→ exposedAddress: \"https://server.mycompany.com:443\"\n 22→\n 23→ # STUN server ports (defaults to [3478] if not specified; set 'stuns' to use external)\n 24→ # stunPorts:\n 25→ # - 3478\n 26→\n 27→ # Metrics endpoint port\n 28→ metricsPort: 9090\n 29→\n 30→ # Healthcheck endpoint address\n 31→ healthcheckAddress: \":9000\"\n 32→\n 33→ # Logging configuration\n 34→ logLevel: \"info\" # Default log level for all components: panic, fatal, error, warn, info, debug, trace\n 35→ logFile: \"console\" # \"console\" or path to log file\n 36→\n 37→ # TLS configuration (optional)\n 38→ tls:\n 39→ certFile: \"\"\n 40→ keyFile: \"\"\n 41→ letsencrypt:\n 42→ enabled: false\n 43→ dataDir: \"\"\n 44→ domains: []\n 45→ email: \"\"\n 46→ awsRoute53: false\n 47→\n 48→ # Shared secret for relay authentication (required when running local relay)\n 49→ authSecret: \"your-secret-key-here\"\n 50→\n 51→ # Data directory for all services\n 52→ dataDir: \"/var/lib/netbird/\"\n 53→\n 54→ # ============================================================================\n 55→ # External Service Overrides (optional)\n 56→ # Use these to point to external Signal, Relay, or STUN servers instead of\n 57→ # running them locally. When set, the corresponding local service is disabled.\n 58→ # ============================================================================\n 59→\n 60→ # External STUN servers - disables local STUN server\n 61→ # stuns:\n 62→ # - uri: \"stun:stun.example.com:3478\"\n 63→ # - uri: \"stun:stun.example.com:3479\"\n 64→\n 65→ # External relay servers - disables local relay server\n 66→ # relays:\n 67→ # addresses:\n 68→ # - \"rels://relay.example.com:443\"\n 69→ # credentialsTTL: \"12h\"\n 70→ # secret: \"relay-shared-secret\"\n 71→\n 72→ # External signal server - disables local signal server\n 73→ # signalUri: \"https://signal.example.com:443\"\n 74→\n 75→ # ============================================================================\n 76→ # Management Settings\n 77→ # ============================================================================\n 78→\n 79→ # Metrics and updates\n 80→ disableAnonymousMetrics: false\n 81→ disableGeoliteUpdate: false\n 82→\n 83→ # Embedded authentication/identity provider (Dex) configuration (always enabled)\n 84→ auth:\n 85→ # OIDC issuer URL - must be publicly accessible\n 86→ issuer: \"https://example.com/oauth2\"\n 87→ localAuthDisabled: false\n 88→ signKeyRefreshEnabled: false\n 89→ # OAuth2 redirect URIs for dashboard\n 90→ dashboardRedirectURIs:\n 91→ - \"https://app.example.com/nb-auth\"\n 92→ - \"https://app.example.com/nb-silent-auth\"\n 93→ # OAuth2 redirect URIs for CLI\n 94→ cliRedirectURIs:\n 95→ - \"http://localhost:53000/\"\n 96→ # Optional initial admin user\n 97→ # owner:\n 98→ # email: \"admin@example.com\"\n 99→ # password: \"initial-password\"\n 100→\n 101→ # Store configuration\n 102→ store:\n 103→ engine: \"sqlite\" # sqlite, postgres, or mysql\n 104→ dsn: \"\" # Connection string for postgres or mysql\n 105→ encryptionKey: \"\"\n 106→\n 107→ # Activity events store configuration (optional, defaults to sqlite in dataDir)\n 108→ # activityStore:\n 109→ # engine: \"sqlite\" # sqlite or postgres\n 110→ # dsn: \"\" # Connection string for postgres\n 111→\n 112→ # Auth (embedded IdP) store configuration (optional, defaults to sqlite3 in dataDir/idp.db)\n 113→ # authStore:\n 114→ # engine: \"sqlite3\" # sqlite3 or postgres\n 115→ # dsn: \"\" # Connection string for postgres (e.g., \"host=localhost port=5432 user=postgres password=postgres dbname=netbird_idp sslmode=disable\")\n 116→\n 117→ # Reverse proxy settings (optional)\n 118→ # reverseProxy:\n 119→ # trustedHTTPProxies: []\n 120→ # trustedHTTPProxiesCount: 0\n 121→ # trustedPeers: []\n 122→"}]},"uuid":"408399db-dd40-4322-8072-d04c8054e82c","timestamp":"2026-03-02T18:56:37.557Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"27934cc2-96d3-44c2-8754-d11a27fc3842","timestamp":"2026-03-02T18:56:37.580Z"}
+{"parentUuid":"27934cc2-96d3-44c2-8754-d11a27fc3842","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"assistant","timestamp":"2026-03-02T18:56:42.237Z","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01MqY6YFCgHz3idW5J6sktNm","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01D5BHQhzbjWxtEhGn3bfLSv","name":"Grep","input":{"pattern":"path\\.Join.*[Dd]ata|path\\.Join.*dir","path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined","output_mode":"content"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":24167,"cache_read_input_tokens":12560,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":24167},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"requestId":"req_011CYemGEfnY2xqDF8hAyiky","uuid":"af0b099c-8a6e-40a5-9175-dc6d3ee573d1"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"a933055c-149c-4d02-b1a2-b9cfccd6a8f7","timestamp":"2026-03-02T18:56:42.238Z"}
+{"parentUuid":"a933055c-149c-4d02-b1a2-b9cfccd6a8f7","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01D5BHQhzbjWxtEhGn3bfLSv","type":"tool_result","content":"combined/cmd/config.go:570:\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\ncombined/cmd/config.go:708:\t\tcfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")"}]},"uuid":"4ec8041b-0a87-4aef-9d8d-641195be9512","timestamp":"2026-03-02T18:56:42.613Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"1e0d4419-d61d-4e95-bfb4-3c6cca2ff576","timestamp":"2026-03-02T18:56:42.617Z"}
+{"parentUuid":"1e0d4419-d61d-4e95-bfb4-3c6cca2ff576","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"assistant","timestamp":"2026-03-02T18:56:42.739Z","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01MqY6YFCgHz3idW5J6sktNm","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01JrMLWchJjBnpxp5WCaUWVo","name":"Grep","input":{"pattern":"\\.DataDir|\\.Datadir|mgmt\\.DataDir","path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined","output_mode":"content"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":24167,"cache_read_input_tokens":12560,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":24167},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"requestId":"req_011CYemGEfnY2xqDF8hAyiky","uuid":"8797181d-999a-4905-847c-2727b1717dd5"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"020cc7b4-8ef9-4e26-a776-8394ceb91f6b","timestamp":"2026-03-02T18:56:42.739Z"}
+{"parentUuid":"020cc7b4-8ef9-4e26-a776-8394ceb91f6b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01JrMLWchJjBnpxp5WCaUWVo","type":"tool_result","content":"combined/cmd/token.go:46:\tdatadir := cfg.Management.DataDir\ncombined/cmd/root.go:455:\t\t\tDataDir: tlsCfg.LetsEncrypt.DataDir,\ncombined/cmd/root.go:468:\t\tcertManager, err := encryption.CreateCertManager(tlsCfg.LetsEncrypt.DataDir, tlsCfg.LetsEncrypt.Domains...)\ncombined/cmd/root.go:613:\tlog.Infof(\" Data dir: %s\", cfg.Server.DataDir)\ncombined/cmd/root.go:651:\tlog.Infof(\" Data dir: %s\", cfg.Management.DataDir)\ncombined/cmd/config.go:338:\tif c.Management.DataDir == \"\" || c.Management.DataDir == \"/var/lib/netbird/\" {\ncombined/cmd/config.go:339:\t\tc.Management.DataDir = c.Server.DataDir\ncombined/cmd/config.go:439:\tif c.Server.DataDir == \"\" {\ncombined/cmd/config.go:472:\t\tc.Server.TLS.LetsEncrypt.DataDir != \"\" &&\ncombined/cmd/config.go:570:\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\ncombined/cmd/config.go:678:\t\tDatadir: mgmt.DataDir,\ncombined/cmd/config.go:707:\tif cfg.EmbeddedIdP.Storage.Config.File == \"\" && cfg.Datadir != \"\" {\ncombined/cmd/config.go:708:\t\tcfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")"}]},"uuid":"969be22e-f60e-409e-9fe8-39d0401a71c3","timestamp":"2026-03-02T18:56:43.102Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"afd0f25edc8d1ce08"},"toolUseID":"agent_msg_015wuzWcWzH97CDEWeHTfjcc","parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","uuid":"fbf5eb75-a4f1-4e63-a242-c29e8db9b1f4","timestamp":"2026-03-02T18:56:43.104Z"}
+{"parentUuid":"fbf5eb75-a4f1-4e63-a242-c29e8db9b1f4","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Agent","command":"entire hooks claude-code post-task"},"parentToolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","toolUseID":"toolu_016XWGFKaFVRWzfti9Nn4icM","timestamp":"2026-03-02T18:56:59.133Z","uuid":"93d218dd-9f6b-415b-b24f-f1d5b0f19a6c"}
+{"parentUuid":"d0838330-7a60-4a59-ad82-7a56dc9816ed","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_016XWGFKaFVRWzfti9Nn4icM","type":"tool_result","content":[{"type":"text","text":"Perfect! Now I have a comprehensive view of how \"datadir\" is used in the combined directory. Let me compile my findings:\n\n## Summary: DataDir Usage in /Users/misha/Documents/GolandProjects/netbird/netbird/combined/\n\n### Overview\nThe combined server uses **two levels of datadir configuration**: `Server.DataDir` (top-level setting) and `Management.DataDir` (internal setting). Both are case-sensitive field names but represent data directories for different purposes.\n\n### Files Involved\n1. **combined/cmd/config.go** - Configuration structures and logic\n2. **combined/cmd/root.go** - Server initialization and logging\n3. **combined/cmd/token.go** - Token command implementation\n4. **combined/config.yaml.example** - Configuration example file\n\n---\n\n### Key Configuration Structures\n\n#### ServerConfig (lines 47-76 in config.go)\n```go\ntype ServerConfig struct {\n // ...\n DataDir string `yaml:\"dataDir\"` // Data directory for all services\n // ...\n}\n```\n- Maps to YAML key: `server.dataDir`\n- Default value: `/var/lib/netbird/`\n- Required field (validated at line 439-440)\n\n#### ManagementConfig (lines 117-131 in config.go)\n```go\ntype ManagementConfig struct {\n Enabled bool `yaml:\"enabled\"`\n LogLevel string `yaml:\"logLevel\"`\n DataDir string `yaml:\"dataDir\"`\n // ...\n}\n```\n- Internal config field (not user-configurable)\n- Automatically populated from Server settings\n\n#### LetsEncryptConfig (lines 86-92 in config.go)\n```go\ntype LetsEncryptConfig struct {\n Enabled bool `yaml:\"enabled\"`\n DataDir string `yaml:\"dataDir\"`\n Domains []string `yaml:\"domains\"`\n Email string `yaml:\"email\"`\n AWSRoute53 bool `yaml:\"awsRoute53\"`\n}\n```\n- Maps to YAML: `server.tls.letsencrypt.dataDir`\n- Used for Let's Encrypt certificate caching\n\n---\n\n### DataDir Flow and Usage\n\n#### 1. **Configuration Loading** (config.go:411-431)\n```go\nfunc LoadConfig(configPath string) (*CombinedConfig, error) {\n cfg := DefaultConfig()\n if configPath == \"\" {\n return cfg, nil\n }\n data, err := os.ReadFile(configPath)\n if err != nil {\n return nil, fmt.Errorf(\"failed to read config file: %w\", err)\n }\n if err := yaml.Unmarshal(data, cfg); err != nil {\n return nil, fmt.Errorf(\"failed to parse config file: %w\", err)\n }\n cfg.ApplySimplifiedDefaults()\n return cfg, nil\n}\n```\n\n#### 2. **Default Values** (config.go:186-230)\n```go\nfunc DefaultConfig() *CombinedConfig {\n return &CombinedConfig{\n Server: ServerConfig{\n // ...\n DataDir: \"/var/lib/netbird/\",\n // ...\n },\n Management: ManagementConfig{\n DataDir: \"/var/lib/netbird/\",\n // ...\n },\n }\n}\n```\n\n#### 3. **Management DataDir Assignment** (config.go:338-340)\n```go\nfunc (c *CombinedConfig) applyManagementDefaults(exposedHost string) {\n c.Management.Enabled = true\n // ...\n if c.Management.DataDir == \"\" || c.Management.DataDir == \"/var/lib/netbird/\" {\n c.Management.DataDir = c.Server.DataDir\n }\n // ...\n}\n```\n- If Management.DataDir is empty or default, it's set to Server.DataDir\n- This allows unified configuration through `server.dataDir`\n\n#### 4. **Validation** (config.go:435-441)\n```go\nfunc (c *CombinedConfig) Validate() error {\n if c.Server.ExposedAddress == \"\" {\n return fmt.Errorf(\"server.exposedAddress is required\")\n }\n if c.Server.DataDir == \"\" {\n return fmt.Errorf(\"server.dataDir is required\")\n }\n // ...\n}\n```\n- `server.dataDir` is mandatory; returns error if empty\n\n---\n\n### DataDir Usage Locations\n\n#### 1. **IDP Storage File Construction** (config.go:569-571)\n```go\nauthStorageFile := \"\"\nif authStorageType == \"postgres\" {\n if authStorageDSN == \"\" {\n return nil, fmt.Errorf(\"authStore.dsn is required when authStore.engine is postgres\")\n }\n} else {\n authStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n}\n```\n- For sqlite3, IDP database is stored at: `{Management.DataDir}/idp.db`\n\n#### 2. **Management Config Construction** (config.go:678)\n```go\nreturn &nbconfig.Config{\n // ...\n Datadir: mgmt.DataDir,\n // ...\n}\n```\n- Note: `Datadir` (lowercase \"dir\") in the nbconfig.Config struct\n\n#### 3. **Embedded IDP Config Application** (config.go:707-708)\n```go\nif cfg.EmbeddedIdP.Storage.Config.File == \"\" && cfg.Datadir != \"\" {\n cfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")\n}\n```\n- Fallback IDP storage file setup during IdP config application\n- Uses the lowercase `Datadir` field from nbconfig.Config\n\n#### 4. **Token Command Store Initialization** (token.go:46-49)\n```go\ndatadir := cfg.Management.DataDir\nengine := types.Engine(cfg.Management.Store.Engine)\n\ns, err := store.NewStore(ctx, engine, datadir, nil, true)\n```\n- Store is created using Management.DataDir as the base path\n- Supports both local (sqlite) and external (postgres/mysql) databases\n\n#### 5. **TLS Let's Encrypt Certificate Caching** (root.go:454-472)\n```go\nr53 := encryption.Route53TLS{\n DataDir: tlsCfg.LetsEncrypt.DataDir,\n Email: tlsCfg.LetsEncrypt.Email,\n Domains: tlsCfg.LetsEncrypt.Domains,\n}\n// ...\ncertManager, err := encryption.CreateCertManager(tlsCfg.LetsEncrypt.DataDir, tlsCfg.LetsEncrypt.Domains...)\n```\n- Separate `LetsEncrypt.DataDir` for certificate storage\n- Used for both AWS Route53 and standard Let's Encrypt flows\n\n---\n\n### YAML Configuration Format (config.yaml.example)\n\n```yaml\nserver:\n # ...\n tls:\n certFile: \"\"\n keyFile: \"\"\n letsencrypt:\n enabled: false\n dataDir: \"\" # Line 43: Let's Encrypt cert storage\n # ...\n \n # Line 52: Main data directory for all services\n dataDir: \"/var/lib/netbird/\"\n```\n\nComments from example:\n- Line 107: \"Activity events store configuration (optional, defaults to sqlite in dataDir)\"\n- Line 112: \"Auth (embedded IdP) store configuration (optional, defaults to sqlite3 in dataDir/idp.db)\"\n\n---\n\n### Logging Output\n\n**Server-level DataDir logging** (root.go:613):\n```go\nlog.Infof(\" Data dir: %s\", cfg.Server.DataDir)\n```\n\n**Management-level DataDir logging** (root.go:651):\n```go\nlog.Infof(\" Data dir: %s\", cfg.Management.DataDir)\n```\n\n---\n\n### Path Construction Pattern\nAll data paths use the standard `path.Join()` function:\n```go\npath.Join(mgmt.DataDir, \"idp.db\") // Database files\npath.Join(cfg.Datadir, \"idp.db\") // Alternative notation\n```\n\nThis ensures proper OS-specific path handling (forward slashes on Unix, backslashes on Windows via path package)."},{"type":"text","text":"agentId: afd0f25edc8d1ce08 (for resuming to continue this agent's work if needed)\n
[:/@])(?P[^/?]+)(?P \\?|$)`)\n 757→\treturn re.ReplaceAllString(dsn, `${pre}`+newDBName+`${post}`)\n 758→}\n 759→\n 760→func LoadSQL(db *gorm.DB, filepath string) error {\n 761→\tsqlContent, err := os.ReadFile(filepath)\n 762→\tif err != nil {\n 763→\t\treturn err\n 764→\t}\n 765→\n 766→\tqueries := strings.Split(string(sqlContent), \";\")\n 767→\n 768→\tfor _, query := range queries {\n 769→\t\tquery = strings.TrimSpace(query)\n 770→\t\tif query != \"\" {\n 771→\t\t\terr := db.Exec(query).Error\n 772→\t\t\tif err != nil {\n 773→\t\t\t\treturn err\n 774→\t\t\t}\n 775→\t\t}\n 776→\t}\n 777→\n 778→\treturn nil\n 779→}\n 780→\n 781→// MigrateFileStoreToSqlite migrates the file store to the SQLite store.\n 782→func MigrateFileStoreToSqlite(ctx context.Context, dataDir string) error {\n 783→\tfileStorePath := path.Join(dataDir, storeFileName)\n 784→\tif _, err := os.Stat(fileStorePath); errors.Is(err, os.ErrNotExist) {\n 785→\t\treturn fmt.Errorf(\"%s doesn't exist, couldn't continue the operation\", fileStorePath)\n 786→\t}\n 787→\n 788→\tsqlStorePath := path.Join(dataDir, storeSqliteFileName)\n 789→\tif _, err := os.Stat(sqlStorePath); err == nil {\n 790→\t\treturn fmt.Errorf(\"%s already exists, couldn't continue the operation\", sqlStorePath)\n 791→\t}\n 792→\n 793→\tfstore, err := NewFileStore(ctx, dataDir, nil)\n 794→\tif err != nil {\n 795→\t\treturn fmt.Errorf(\"failed creating file store: %s: %v\", dataDir, err)\n 796→\t}\n 797→\n 798→\tfsStoreAccounts := len(fstore.GetAllAccounts(ctx))\n 799→\tlog.WithContext(ctx).Infof(\"%d account will be migrated from file store %s to sqlite store %s\",\n 800→\t\tfsStoreAccounts, fileStorePath, sqlStorePath)\n 801→\n 802→\tstore, err := NewSqliteStoreFromFileStore(ctx, fstore, dataDir, nil, true)\n 803→\tif err != nil {\n 804→\t\treturn fmt.Errorf(\"failed creating file store: %s: %v\", dataDir, err)\n 805→\t}\n 806→\n 807→\tsqliteStoreAccounts := len(store.GetAllAccounts(ctx))\n 808→\tif fsStoreAccounts != sqliteStoreAccounts {\n 809→\t\treturn fmt.Errorf(\"failed to migratePreAuto accounts from file to sqlite. Expected accounts: %d, got: %d\",\n 810→\t\t\tfsStoreAccounts, sqliteStoreAccounts)\n 811→\t}\n 812→\n 813→\treturn nil\n 814→}\n 815→"}]},"uuid":"f7dc6630-18ce-4d41-a740-d7ff55dbff21","timestamp":"2026-03-02T19:04:34.858Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"a56befe92f328ad5f"},"toolUseID":"agent_msg_015MDxkSDakHg89MjVmWvshB","parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","uuid":"663f56a6-5965-47c1-9d4f-c07718220f7e","timestamp":"2026-03-02T19:04:34.968Z"} +{"parentUuid":"663f56a6-5965-47c1-9d4f-c07718220f7e","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"assistant","timestamp":"2026-03-02T19:04:35.140Z","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01S5wH5nhhTqpxBi395bMKzm","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01FZ3nJNP5kcPn6mrNV71oKV","name":"Grep","input":{"pattern":"ActivityStore|activityStore","path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server","output_mode":"files_with_matches"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":10419,"cache_read_input_tokens":14860,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":10419},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"requestId":"req_011CYemsGB46QvvgJSQjFt8v","uuid":"0673d201-8a83-4d38-a088-94ef510944fa"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"a56befe92f328ad5f"},"toolUseID":"agent_msg_015MDxkSDakHg89MjVmWvshB","parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","uuid":"25b4fcef-e867-43e7-a92a-e30e91d7d1a9","timestamp":"2026-03-02T19:04:35.141Z"} +{"parentUuid":"25b4fcef-e867-43e7-a92a-e30e91d7d1a9","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01FZ3nJNP5kcPn6mrNV71oKV","type":"tool_result","content":"No files found"}]},"uuid":"e1cf7650-0e64-4aab-92bd-f34c66168c3c","timestamp":"2026-03-02T19:04:35.554Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"a56befe92f328ad5f"},"toolUseID":"agent_msg_015MDxkSDakHg89MjVmWvshB","parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","uuid":"84fa0d51-698e-41e1-9a43-db0e52e9d46a","timestamp":"2026-03-02T19:04:35.555Z"} +{"parentUuid":"84fa0d51-698e-41e1-9a43-db0e52e9d46a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"assistant","timestamp":"2026-03-02T19:04:37.227Z","message":{"model":"claude-haiku-4-5-20251001","id":"msg_015KVPitz4tom3eobUTg2WNx","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01V8hWnUoPyjUdxpxDbv8wCf","name":"Glob","input":{"pattern":"**/management/internals/server/server.go"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":15750,"cache_read_input_tokens":25279,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":15750},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"requestId":"req_011CYemsUBcGe33WAvpaTdCC","uuid":"5fe597b8-1532-4d99-80d5-47a49f227ecd"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"a56befe92f328ad5f"},"toolUseID":"agent_msg_015MDxkSDakHg89MjVmWvshB","parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","uuid":"8e880360-913d-44e4-8c56-60796c80720e","timestamp":"2026-03-02T19:04:37.228Z"} +{"parentUuid":"8e880360-913d-44e4-8c56-60796c80720e","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01V8hWnUoPyjUdxpxDbv8wCf","type":"tool_result","content":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/server.go"}]},"uuid":"a22ba0bc-d636-4fe2-b3e2-d08c94da06e4","timestamp":"2026-03-02T19:04:37.625Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"a56befe92f328ad5f"},"toolUseID":"agent_msg_015MDxkSDakHg89MjVmWvshB","parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","uuid":"d8d65208-ba53-4bf1-beca-89ea3e985cb9","timestamp":"2026-03-02T19:04:37.630Z"} +{"parentUuid":"d8d65208-ba53-4bf1-beca-89ea3e985cb9","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"assistant","timestamp":"2026-03-02T19:04:38.013Z","message":{"model":"claude-haiku-4-5-20251001","id":"msg_015KVPitz4tom3eobUTg2WNx","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01CarLa9FZ2hEcyeQ1VnKWug","name":"Grep","input":{"pattern":"activityStore|ActivityStore|activity\\.Store|NewActivityStore","path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management","output_mode":"files_with_matches"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":15750,"cache_read_input_tokens":25279,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":15750},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"requestId":"req_011CYemsUBcGe33WAvpaTdCC","uuid":"8b1a4d62-8f22-4f9b-8d53-43146ae6bc87"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"a56befe92f328ad5f"},"toolUseID":"agent_msg_015MDxkSDakHg89MjVmWvshB","parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","uuid":"fab03065-b4bc-4c36-81e4-8b1ba6ab3d67","timestamp":"2026-03-02T19:04:38.014Z"} +{"parentUuid":"fab03065-b4bc-4c36-81e4-8b1ba6ab3d67","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01CarLa9FZ2hEcyeQ1VnKWug","type":"tool_result","content":"Found 3 files\nmanagement/server/activity/store/sql_store.go\nmanagement/server/account.go\nmanagement/internals/server/boot.go"}]},"uuid":"47d24c91-2759-486f-b094-61db1140d524","timestamp":"2026-03-02T19:04:38.397Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"a56befe92f328ad5f"},"toolUseID":"agent_msg_015MDxkSDakHg89MjVmWvshB","parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","uuid":"387f589d-8a6d-4833-b0d1-faee63c91c49","timestamp":"2026-03-02T19:04:38.399Z"} +{"parentUuid":"387f589d-8a6d-4833-b0d1-faee63c91c49","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"assistant","timestamp":"2026-03-02T19:04:39.956Z","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01597DKzvaxnJp9p7BfnZ1Vj","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Ta8fi1sbT1p4GAXcrAwgFr","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/boot.go"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":348,"cache_read_input_tokens":41029,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":348},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"requestId":"req_011CYemsgLbuz5fQGnQDxtE1","uuid":"b1f4c614-51bf-46a1-b218-1a924fc28686"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"a56befe92f328ad5f"},"toolUseID":"agent_msg_015MDxkSDakHg89MjVmWvshB","parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","uuid":"7a38f300-8808-4288-988f-32b4391a0b35","timestamp":"2026-03-02T19:04:39.957Z"} +{"parentUuid":"7a38f300-8808-4288-988f-32b4391a0b35","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Ta8fi1sbT1p4GAXcrAwgFr","type":"tool_result","content":" 1→package server\n 2→\n 3→// @note this file includes all the lower level dependencies, db, http and grpc BaseServer, metrics, logger, etc.\n 4→\n 5→import (\n 6→\t\"context\"\n 7→\t\"crypto/tls\"\n 8→\t\"net/http\"\n 9→\t\"net/netip\"\n 10→\t\"slices\"\n 11→\t\"time\"\n 12→\n 13→\tgrpcMiddleware \"github.com/grpc-ecosystem/go-grpc-middleware/v2\"\n 14→\t\"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip\"\n 15→\t\"github.com/rs/xid\"\n 16→\tlog \"github.com/sirupsen/logrus\"\n 17→\t\"google.golang.org/grpc\"\n 18→\t\"google.golang.org/grpc/credentials\"\n 19→\t\"google.golang.org/grpc/keepalive\"\n 20→\n 21→\t\"github.com/netbirdio/management-integrations/integrations\"\n 22→\t\"github.com/netbirdio/netbird/encryption\"\n 23→\t\"github.com/netbirdio/netbird/formatter/hook\"\n 24→\t\"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs\"\n 25→\taccesslogsmanager \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs/manager\"\n 26→\tnbgrpc \"github.com/netbirdio/netbird/management/internals/shared/grpc\"\n 27→\t\"github.com/netbirdio/netbird/management/server/activity\"\n 28→\tnbContext \"github.com/netbirdio/netbird/management/server/context\"\n 29→\tnbhttp \"github.com/netbirdio/netbird/management/server/http\"\n 30→\t\"github.com/netbirdio/netbird/management/server/store\"\n 31→\t\"github.com/netbirdio/netbird/management/server/telemetry\"\n 32→\tmgmtProto \"github.com/netbirdio/netbird/shared/management/proto\"\n 33→\t\"github.com/netbirdio/netbird/util/crypt\"\n 34→)\n 35→\n 36→var (\n 37→\tkaep = keepalive.EnforcementPolicy{\n 38→\t\tMinTime: 15 * time.Second,\n 39→\t\tPermitWithoutStream: true,\n 40→\t}\n 41→\n 42→\tkasp = keepalive.ServerParameters{\n 43→\t\tMaxConnectionIdle: 15 * time.Second,\n 44→\t\tMaxConnectionAgeGrace: 5 * time.Second,\n 45→\t\tTime: 5 * time.Second,\n 46→\t\tTimeout: 2 * time.Second,\n 47→\t}\n 48→)\n 49→\n 50→func (s *BaseServer) Metrics() telemetry.AppMetrics {\n 51→\treturn Create(s, func() telemetry.AppMetrics {\n 52→\t\tappMetrics, err := telemetry.NewDefaultAppMetrics(context.Background())\n 53→\t\tif err != nil {\n 54→\t\t\tlog.Fatalf(\"error while creating app metrics: %s\", err)\n 55→\t\t}\n 56→\t\treturn appMetrics\n 57→\t})\n 58→}\n 59→\n 60→func (s *BaseServer) Store() store.Store {\n 61→\treturn Create(s, func() store.Store {\n 62→\t\tstore, err := store.NewStore(context.Background(), s.Config.StoreConfig.Engine, s.Config.Datadir, s.Metrics(), false)\n 63→\t\tif err != nil {\n 64→\t\t\tlog.Fatalf(\"failed to create store: %v\", err)\n 65→\t\t}\n 66→\n 67→\t\tif s.Config.DataStoreEncryptionKey != \"\" {\n 68→\t\t\tfieldEncrypt, err := crypt.NewFieldEncrypt(s.Config.DataStoreEncryptionKey)\n 69→\t\t\tif err != nil {\n 70→\t\t\t\tlog.Fatalf(\"failed to create field encryptor: %v\", err)\n 71→\t\t\t}\n 72→\t\t\tstore.SetFieldEncrypt(fieldEncrypt)\n 73→\t\t}\n 74→\n 75→\t\treturn store\n 76→\t})\n 77→}\n 78→\n 79→func (s *BaseServer) EventStore() activity.Store {\n 80→\treturn Create(s, func() activity.Store {\n 81→\t\tintegrationMetrics, err := integrations.InitIntegrationMetrics(context.Background(), s.Metrics())\n 82→\t\tif err != nil {\n 83→\t\t\tlog.Fatalf(\"failed to initialize integration metrics: %v\", err)\n 84→\t\t}\n 85→\n 86→\t\teventStore, _, err := integrations.InitEventStore(context.Background(), s.Config.Datadir, s.Config.DataStoreEncryptionKey, integrationMetrics)\n 87→\t\tif err != nil {\n 88→\t\t\tlog.Fatalf(\"failed to initialize event store: %v\", err)\n 89→\t\t}\n 90→\n 91→\t\treturn eventStore\n 92→\t})\n 93→}\n 94→\n 95→func (s *BaseServer) APIHandler() http.Handler {\n 96→\treturn Create(s, func() http.Handler {\n 97→\t\thttpAPIHandler, err := nbhttp.NewAPIHandler(context.Background(), s.AccountManager(), s.NetworksManager(), s.ResourcesManager(), s.RoutesManager(), s.GroupsManager(), s.GeoLocationManager(), s.AuthManager(), s.Metrics(), s.IntegratedValidator(), s.ProxyController(), s.PermissionsManager(), s.PeersManager(), s.SettingsManager(), s.ZonesManager(), s.RecordsManager(), s.NetworkMapController(), s.IdpManager(), s.ReverseProxyManager(), s.ReverseProxyDomainManager(), s.AccessLogsManager(), s.ReverseProxyGRPCServer(), s.Config.ReverseProxy.TrustedHTTPProxies)\n 98→\t\tif err != nil {\n 99→\t\t\tlog.Fatalf(\"failed to create API handler: %v\", err)\n 100→\t\t}\n 101→\t\treturn httpAPIHandler\n 102→\t})\n 103→}\n 104→\n 105→func (s *BaseServer) GRPCServer() *grpc.Server {\n 106→\treturn Create(s, func() *grpc.Server {\n 107→\t\ttrustedPeers := s.Config.ReverseProxy.TrustedPeers\n 108→\t\tdefaultTrustedPeers := []netip.Prefix{netip.MustParsePrefix(\"0.0.0.0/0\"), netip.MustParsePrefix(\"::/0\")}\n 109→\t\tif len(trustedPeers) == 0 || slices.Equal[[]netip.Prefix](trustedPeers, defaultTrustedPeers) {\n 110→\t\t\tlog.WithContext(context.Background()).Warn(\"TrustedPeers are configured to default value '0.0.0.0/0', '::/0'. This allows connection IP spoofing.\")\n 111→\t\t\ttrustedPeers = defaultTrustedPeers\n 112→\t\t}\n 113→\t\ttrustedHTTPProxies := s.Config.ReverseProxy.TrustedHTTPProxies\n 114→\t\ttrustedProxiesCount := s.Config.ReverseProxy.TrustedHTTPProxiesCount\n 115→\t\tif len(trustedHTTPProxies) > 0 && trustedProxiesCount > 0 {\n 116→\t\t\tlog.WithContext(context.Background()).Warn(\"TrustedHTTPProxies and TrustedHTTPProxiesCount both are configured. \" +\n 117→\t\t\t\t\"This is not recommended way to extract X-Forwarded-For. Consider using one of these options.\")\n 118→\t\t}\n 119→\t\trealipOpts := []realip.Option{\n 120→\t\t\trealip.WithTrustedPeers(trustedPeers),\n 121→\t\t\trealip.WithTrustedProxies(trustedHTTPProxies),\n 122→\t\t\trealip.WithTrustedProxiesCount(trustedProxiesCount),\n 123→\t\t\trealip.WithHeaders([]string{realip.XForwardedFor, realip.XRealIp}),\n 124→\t\t}\n 125→\t\tproxyUnary, proxyStream, proxyAuthClose := nbgrpc.NewProxyAuthInterceptors(s.Store())\n 126→\t\ts.proxyAuthClose = proxyAuthClose\n 127→\t\tgRPCOpts := []grpc.ServerOption{\n 128→\t\t\tgrpc.KeepaliveEnforcementPolicy(kaep),\n 129→\t\t\tgrpc.KeepaliveParams(kasp),\n 130→\t\t\tgrpc.ChainUnaryInterceptor(realip.UnaryServerInterceptorOpts(realipOpts...), unaryInterceptor, proxyUnary),\n 131→\t\t\tgrpc.ChainStreamInterceptor(realip.StreamServerInterceptorOpts(realipOpts...), streamInterceptor, proxyStream),\n 132→\t\t}\n 133→\n 134→\t\tif s.Config.HttpConfig.LetsEncryptDomain != \"\" {\n 135→\t\t\tcertManager, err := encryption.CreateCertManager(s.Config.Datadir, s.Config.HttpConfig.LetsEncryptDomain)\n 136→\t\t\tif err != nil {\n 137→\t\t\t\tlog.Fatalf(\"failed to create certificate manager: %v\", err)\n 138→\t\t\t}\n 139→\t\t\ttransportCredentials := credentials.NewTLS(certManager.TLSConfig())\n 140→\t\t\tgRPCOpts = append(gRPCOpts, grpc.Creds(transportCredentials))\n 141→\t\t} else if s.Config.HttpConfig.CertFile != \"\" && s.Config.HttpConfig.CertKey != \"\" {\n 142→\t\t\ttlsConfig, err := loadTLSConfig(s.Config.HttpConfig.CertFile, s.Config.HttpConfig.CertKey)\n 143→\t\t\tif err != nil {\n 144→\t\t\t\tlog.Fatalf(\"cannot load TLS credentials: %v\", err)\n 145→\t\t\t}\n 146→\t\t\ttransportCredentials := credentials.NewTLS(tlsConfig)\n 147→\t\t\tgRPCOpts = append(gRPCOpts, grpc.Creds(transportCredentials))\n 148→\t\t}\n 149→\n 150→\t\tgRPCAPIHandler := grpc.NewServer(gRPCOpts...)\n 151→\t\tsrv, err := nbgrpc.NewServer(s.Config, s.AccountManager(), s.SettingsManager(), s.JobManager(), s.SecretsManager(), s.Metrics(), s.AuthManager(), s.IntegratedValidator(), s.NetworkMapController(), s.OAuthConfigProvider())\n 152→\t\tif err != nil {\n 153→\t\t\tlog.Fatalf(\"failed to create management server: %v\", err)\n 154→\t\t}\n 155→\t\treverseProxyMgr := s.ReverseProxyManager()\n 156→\t\tsrv.SetReverseProxyManager(reverseProxyMgr)\n 157→\t\tif reverseProxyMgr != nil {\n 158→\t\t\treverseProxyMgr.StartExposeReaper(context.Background())\n 159→\t\t}\n 160→\t\tmgmtProto.RegisterManagementServiceServer(gRPCAPIHandler, srv)\n 161→\n 162→\t\tmgmtProto.RegisterProxyServiceServer(gRPCAPIHandler, s.ReverseProxyGRPCServer())\n 163→\t\tlog.Info(\"ProxyService registered on gRPC server\")\n 164→\n 165→\t\treturn gRPCAPIHandler\n 166→\t})\n 167→}\n 168→\n 169→func (s *BaseServer) ReverseProxyGRPCServer() *nbgrpc.ProxyServiceServer {\n 170→\treturn Create(s, func() *nbgrpc.ProxyServiceServer {\n 171→\t\tproxyService := nbgrpc.NewProxyServiceServer(s.AccessLogsManager(), s.ProxyTokenStore(), s.proxyOIDCConfig(), s.PeersManager(), s.UsersManager())\n 172→\t\ts.AfterInit(func(s *BaseServer) {\n 173→\t\t\tproxyService.SetProxyManager(s.ReverseProxyManager())\n 174→\t\t})\n 175→\t\treturn proxyService\n 176→\t})\n 177→}\n 178→\n 179→func (s *BaseServer) proxyOIDCConfig() nbgrpc.ProxyOIDCConfig {\n 180→\treturn Create(s, func() nbgrpc.ProxyOIDCConfig {\n 181→\t\treturn nbgrpc.ProxyOIDCConfig{\n 182→\t\t\tIssuer: s.Config.HttpConfig.AuthIssuer,\n 183→\t\t\t// todo: double check auth clientID value\n 184→\t\t\tClientID: s.Config.HttpConfig.AuthClientID, // Reuse dashboard client\n 185→\t\t\tScopes: []string{\"openid\", \"profile\", \"email\"},\n 186→\t\t\tCallbackURL: s.Config.HttpConfig.AuthCallbackURL,\n 187→\t\t\tHMACKey: []byte(s.Config.DataStoreEncryptionKey), // Use the datastore encryption key for OIDC state HMACs, this should ensure all management instances are using the same key.\n 188→\t\t\tAudience: s.Config.HttpConfig.AuthAudience,\n 189→\t\t\tKeysLocation: s.Config.HttpConfig.AuthKeysLocation,\n 190→\t\t}\n 191→\t})\n 192→}\n 193→\n 194→func (s *BaseServer) ProxyTokenStore() *nbgrpc.OneTimeTokenStore {\n 195→\treturn Create(s, func() *nbgrpc.OneTimeTokenStore {\n 196→\t\ttokenStore := nbgrpc.NewOneTimeTokenStore(1 * time.Minute)\n 197→\t\tlog.Info(\"One-time token store initialized for proxy authentication\")\n 198→\t\treturn tokenStore\n 199→\t})\n 200→}\n 201→\n 202→func (s *BaseServer) AccessLogsManager() accesslogs.Manager {\n 203→\treturn Create(s, func() accesslogs.Manager {\n 204→\t\taccessLogManager := accesslogsmanager.NewManager(s.Store(), s.PermissionsManager(), s.GeoLocationManager())\n 205→\t\taccessLogManager.StartPeriodicCleanup(\n 206→\t\t\tcontext.Background(),\n 207→\t\t\ts.Config.ReverseProxy.AccessLogRetentionDays,\n 208→\t\t\ts.Config.ReverseProxy.AccessLogCleanupIntervalHours,\n 209→\t\t)\n 210→\t\treturn accessLogManager\n 211→\t})\n 212→}\n 213→\n 214→func loadTLSConfig(certFile string, certKey string) (*tls.Config, error) {\n 215→\t// Load server's certificate and private key\n 216→\tserverCert, err := tls.LoadX509KeyPair(certFile, certKey)\n 217→\tif err != nil {\n 218→\t\treturn nil, err\n 219→\t}\n 220→\n 221→\t// NewDefaultAppMetrics the credentials and return it\n 222→\tconfig := &tls.Config{\n 223→\t\tCertificates: []tls.Certificate{serverCert},\n 224→\t\tClientAuth: tls.NoClientCert,\n 225→\t\tNextProtos: []string{\n 226→\t\t\t\"h2\", \"http/1.1\", // enable HTTP/2\n 227→\t\t},\n 228→\t}\n 229→\n 230→\treturn config, nil\n 231→}\n 232→\n 233→func unaryInterceptor(\n 234→\tctx context.Context,\n 235→\treq interface{},\n 236→\tinfo *grpc.UnaryServerInfo,\n 237→\thandler grpc.UnaryHandler,\n 238→) (interface{}, error) {\n 239→\treqID := xid.New().String()\n 240→\t//nolint\n 241→\tctx = context.WithValue(ctx, hook.ExecutionContextKey, hook.GRPCSource)\n 242→\t//nolint\n 243→\tctx = context.WithValue(ctx, nbContext.RequestIDKey, reqID)\n 244→\treturn handler(ctx, req)\n 245→}\n 246→\n 247→func streamInterceptor(\n 248→\tsrv interface{},\n 249→\tss grpc.ServerStream,\n 250→\tinfo *grpc.StreamServerInfo,\n 251→\thandler grpc.StreamHandler,\n 252→) error {\n 253→\treqID := xid.New().String()\n 254→\twrapped := grpcMiddleware.WrapServerStream(ss)\n 255→\t//nolint\n 256→\tctx := context.WithValue(ss.Context(), hook.ExecutionContextKey, hook.GRPCSource)\n 257→\t//nolint\n 258→\twrapped.WrappedContext = context.WithValue(ctx, nbContext.RequestIDKey, reqID)\n 259→\treturn handler(srv, wrapped)\n 260→}\n 261→"}]},"uuid":"d8dc2a02-8bc3-408d-b8ce-7fe48ed17e2d","timestamp":"2026-03-02T19:04:39.975Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"a56befe92f328ad5f"},"toolUseID":"agent_msg_015MDxkSDakHg89MjVmWvshB","parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","uuid":"3d1c654b-4737-4d6d-be9c-1a4a4a31ee28","timestamp":"2026-03-02T19:04:40.160Z"} +{"parentUuid":"3d1c654b-4737-4d6d-be9c-1a4a4a31ee28","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"assistant","timestamp":"2026-03-02T19:04:40.336Z","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01597DKzvaxnJp9p7BfnZ1Vj","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01FrUgeLQUYdQct6zYawmpht","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/account.go","limit":100},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":348,"cache_read_input_tokens":41029,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":348},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"requestId":"req_011CYemsgLbuz5fQGnQDxtE1","uuid":"f1b67287-3e03-4d2d-82fd-da5e3d430499"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"a56befe92f328ad5f"},"toolUseID":"agent_msg_015MDxkSDakHg89MjVmWvshB","parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","uuid":"86667ff5-c0a5-4223-a5a3-a34a80e220de","timestamp":"2026-03-02T19:04:40.336Z"} +{"parentUuid":"86667ff5-c0a5-4223-a5a3-a34a80e220de","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01FrUgeLQUYdQct6zYawmpht","type":"tool_result","content":" 1→package server\n 2→\n 3→import (\n 4→\t\"context\"\n 5→\t\"errors\"\n 6→\t\"fmt\"\n 7→\t\"math/rand\"\n 8→\t\"net\"\n 9→\t\"net/netip\"\n 10→\t\"os\"\n 11→\t\"reflect\"\n 12→\t\"regexp\"\n 13→\t\"slices\"\n 14→\t\"strings\"\n 15→\t\"sync\"\n 16→\t\"time\"\n 17→\n 18→\t\"github.com/netbirdio/netbird/management/internals/modules/reverseproxy\"\n 19→\t\"github.com/netbirdio/netbird/management/server/job\"\n 20→\t\"github.com/netbirdio/netbird/shared/auth\"\n 21→\n 22→\tcacheStore \"github.com/eko/gocache/lib/v4/store\"\n 23→\t\"github.com/eko/gocache/store/redis/v4\"\n 24→\t\"github.com/rs/xid\"\n 25→\tlog \"github.com/sirupsen/logrus\"\n 26→\t\"github.com/vmihailenco/msgpack/v5\"\n 27→\t\"golang.org/x/exp/maps\"\n 28→\n 29→\tnbdns \"github.com/netbirdio/netbird/dns\"\n 30→\t\"github.com/netbirdio/netbird/formatter/hook\"\n 31→\t\"github.com/netbirdio/netbird/management/internals/controllers/network_map\"\n 32→\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n 33→\t\"github.com/netbirdio/netbird/management/server/account\"\n 34→\t\"github.com/netbirdio/netbird/management/server/activity\"\n 35→\tnbcache \"github.com/netbirdio/netbird/management/server/cache\"\n 36→\tnbcontext \"github.com/netbirdio/netbird/management/server/context\"\n 37→\t\"github.com/netbirdio/netbird/management/server/geolocation\"\n 38→\t\"github.com/netbirdio/netbird/management/server/idp\"\n 39→\t\"github.com/netbirdio/netbird/management/server/integrations/integrated_validator\"\n 40→\t\"github.com/netbirdio/netbird/management/server/integrations/port_forwarding\"\n 41→\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n 42→\t\"github.com/netbirdio/netbird/management/server/permissions\"\n 43→\t\"github.com/netbirdio/netbird/management/server/permissions/modules\"\n 44→\t\"github.com/netbirdio/netbird/management/server/permissions/operations\"\n 45→\t\"github.com/netbirdio/netbird/management/server/posture\"\n 46→\t\"github.com/netbirdio/netbird/management/server/settings\"\n 47→\t\"github.com/netbirdio/netbird/management/server/store\"\n 48→\t\"github.com/netbirdio/netbird/management/server/telemetry\"\n 49→\t\"github.com/netbirdio/netbird/management/server/types\"\n 50→\t\"github.com/netbirdio/netbird/management/server/util\"\n 51→\t\"github.com/netbirdio/netbird/route\"\n 52→\tnbdomain \"github.com/netbirdio/netbird/shared/management/domain\"\n 53→\t\"github.com/netbirdio/netbird/shared/management/status\"\n 54→)\n 55→\n 56→const (\n 57→\tpeerSchedulerRetryInterval = 3 * time.Second\n 58→\temptyUserID = \"empty user ID in claims\"\n 59→\terrorGettingDomainAccIDFmt = \"error getting account ID by private domain: %v\"\n 60→)\n 61→\n 62→type userLoggedInOnce bool\n 63→\n 64→func cacheEntryExpiration() time.Duration {\n 65→\tr := rand.Intn(int(nbcache.DefaultIDPCacheExpirationMax.Milliseconds()-nbcache.DefaultIDPCacheExpirationMin.Milliseconds())) + int(nbcache.DefaultIDPCacheExpirationMin.Milliseconds())\n 66→\treturn time.Duration(r) * time.Millisecond\n 67→}\n 68→\n 69→type DefaultAccountManager struct {\n 70→\tStore store.Store\n 71→\t// cacheMux and cacheLoading helps to make sure that only a single cache reload runs at a time per accountID\n 72→\tcacheMux sync.Mutex\n 73→\t// cacheLoading keeps the accountIDs that are currently reloading. The accountID has to be removed once cache has been reloaded\n 74→\tcacheLoading map[string]chan struct{}\n 75→\tnetworkMapController network_map.Controller\n 76→\tjobManager *job.Manager\n 77→\tidpManager idp.Manager\n 78→\tcacheManager *nbcache.AccountUserDataCache\n 79→\texternalCacheManager nbcache.UserDataCache\n 80→\tctx context.Context\n 81→\teventStore activity.Store\n 82→\tgeo geolocation.Geolocation\n 83→\n 84→\trequestBuffer *AccountRequestBuffer\n 85→\n 86→\tproxyController port_forwarding.Controller\n 87→\tsettingsManager settings.Manager\n 88→\treverseProxyManager reverseproxy.Manager\n 89→\n 90→\t// config contains the management server configuration\n 91→\tconfig *nbconfig.Config\n 92→\n 93→\t// singleAccountMode indicates whether the instance has a single account.\n 94→\t// If true, then every new user will end up under the same account.\n 95→\t// This value will be set to false if management service has more than one account.\n 96→\tsingleAccountMode bool\n 97→\t// singleAccountModeDomain is a domain to use in singleAccountMode setup\n 98→\tsingleAccountModeDomain string\n 99→\n 100→\tpeerLoginExpiry Scheduler"}]},"uuid":"3e86ebe9-31f9-4fb0-b6e8-3a92963e08c6","timestamp":"2026-03-02T19:04:40.350Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"a56befe92f328ad5f"},"toolUseID":"agent_msg_015MDxkSDakHg89MjVmWvshB","parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","uuid":"6185bbc2-974b-41f5-9941-03d314e73997","timestamp":"2026-03-02T19:04:40.362Z"} +{"parentUuid":"6185bbc2-974b-41f5-9941-03d314e73997","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"assistant","timestamp":"2026-03-02T19:04:40.378Z","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01597DKzvaxnJp9p7BfnZ1Vj","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_017sXHeJqkJb1m3TpFigV7er","name":"Glob","input":{"pattern":"**/management/server/activity/store/**"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":348,"cache_read_input_tokens":41029,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":348},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"requestId":"req_011CYemsgLbuz5fQGnQDxtE1","uuid":"40e0695e-d623-441d-8af1-46e8e7e65d7c"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"a56befe92f328ad5f"},"toolUseID":"agent_msg_015MDxkSDakHg89MjVmWvshB","parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","uuid":"86e3cfd4-d58d-41ce-a01e-0f1bc771ec8f","timestamp":"2026-03-02T19:04:40.378Z"} +{"parentUuid":"86e3cfd4-d58d-41ce-a01e-0f1bc771ec8f","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_017sXHeJqkJb1m3TpFigV7er","type":"tool_result","content":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/migration.go\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/migration_test.go\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store_test.go\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go"}]},"uuid":"9ecafd0f-1322-401c-ad00-e230c22c0e7d","timestamp":"2026-03-02T19:04:40.772Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"a56befe92f328ad5f"},"toolUseID":"agent_msg_015MDxkSDakHg89MjVmWvshB","parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","uuid":"77b2dd42-f15e-4599-bd39-9bafca1887de","timestamp":"2026-03-02T19:04:40.773Z"} +{"parentUuid":"77b2dd42-f15e-4599-bd39-9bafca1887de","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"assistant","timestamp":"2026-03-02T19:04:42.332Z","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01BF2fN3b4n1zpy61qjEEQng","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01HGGafAZCjF9FJTSuc59ALq","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go","limit":150},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":6544,"cache_read_input_tokens":41377,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":6544},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"requestId":"req_011CYemsrV3rN3Z4UdMibTyH","uuid":"cbfe456e-de1a-4b57-8a03-2d90d4af8fea"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"a56befe92f328ad5f"},"toolUseID":"agent_msg_015MDxkSDakHg89MjVmWvshB","parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","uuid":"abf3cc0f-19a9-401d-91bf-7c35cc26bfbf","timestamp":"2026-03-02T19:04:42.333Z"} +{"parentUuid":"abf3cc0f-19a9-401d-91bf-7c35cc26bfbf","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01HGGafAZCjF9FJTSuc59ALq","type":"tool_result","content":" 1→package store\n 2→\n 3→import (\n 4→\t\"context\"\n 5→\t\"fmt\"\n 6→\t\"os\"\n 7→\t\"path/filepath\"\n 8→\t\"runtime\"\n 9→\t\"strconv\"\n 10→\t\"time\"\n 11→\n 12→\tlog \"github.com/sirupsen/logrus\"\n 13→\t\"gorm.io/driver/postgres\"\n 14→\t\"gorm.io/driver/sqlite\"\n 15→\t\"gorm.io/gorm\"\n 16→\t\"gorm.io/gorm/clause\"\n 17→\t\"gorm.io/gorm/logger\"\n 18→\n 19→\t\"github.com/netbirdio/netbird/management/server/activity\"\n 20→\t\"github.com/netbirdio/netbird/management/server/types\"\n 21→\t\"github.com/netbirdio/netbird/util/crypt\"\n 22→)\n 23→\n 24→const (\n 25→\t// eventSinkDB is the default name of the events database\n 26→\teventSinkDB = \"events.db\"\n 27→\n 28→\tfallbackName = \"unknown\"\n 29→\tfallbackEmail = \"unknown@unknown.com\"\n 30→\n 31→\tgcmEncAlgo = \"GCM\"\n 32→\n 33→\tstoreEngineEnv = \"NB_ACTIVITY_EVENT_STORE_ENGINE\"\n 34→\tpostgresDsnEnv = \"NB_ACTIVITY_EVENT_POSTGRES_DSN\"\n 35→\tsqlMaxOpenConnsEnv = \"NB_SQL_MAX_OPEN_CONNS\"\n 36→)\n 37→\n 38→type eventWithNames struct {\n 39→\tactivity.Event\n 40→\tInitiatorName string\n 41→\tInitiatorEmail string\n 42→\tTargetName string\n 43→\tTargetEmail string\n 44→}\n 45→\n 46→// Store is the implementation of the activity.Store interface backed by SQLite\n 47→type Store struct {\n 48→\tdb *gorm.DB\n 49→\tfieldEncrypt *crypt.FieldEncrypt\n 50→}\n 51→\n 52→// NewSqlStore creates a new Store with an event table if not exists.\n 53→func NewSqlStore(ctx context.Context, dataDir string, encryptionKey string) (*Store, error) {\n 54→\tfieldEncrypt, err := crypt.NewFieldEncrypt(encryptionKey)\n 55→\tif err != nil {\n 56→\n 57→\t\treturn nil, err\n 58→\t}\n 59→\n 60→\tdb, err := initDatabase(ctx, dataDir)\n 61→\tif err != nil {\n 62→\t\treturn nil, fmt.Errorf(\"initialize database: %w\", err)\n 63→\t}\n 64→\n 65→\tif err = migrate(ctx, fieldEncrypt, db); err != nil {\n 66→\t\treturn nil, fmt.Errorf(\"events database migration: %w\", err)\n 67→\t}\n 68→\n 69→\terr = db.AutoMigrate(&activity.Event{}, &activity.DeletedUser{})\n 70→\tif err != nil {\n 71→\t\treturn nil, fmt.Errorf(\"events auto migrate: %w\", err)\n 72→\t}\n 73→\n 74→\treturn &Store{\n 75→\t\tdb: db,\n 76→\t\tfieldEncrypt: fieldEncrypt,\n 77→\t}, nil\n 78→}\n 79→\n 80→func (store *Store) processResult(ctx context.Context, events []*eventWithNames) ([]*activity.Event, error) {\n 81→\tactivityEvents := make([]*activity.Event, 0)\n 82→\tvar cryptErr error\n 83→\n 84→\tfor _, event := range events {\n 85→\t\te := event.Event\n 86→\t\tif e.Meta == nil {\n 87→\t\t\te.Meta = make(map[string]any)\n 88→\t\t}\n 89→\n 90→\t\tif event.TargetName != \"\" {\n 91→\t\t\tname, err := store.fieldEncrypt.Decrypt(event.TargetName)\n 92→\t\t\tif err != nil {\n 93→\t\t\t\tcryptErr = fmt.Errorf(\"failed to decrypt username for target id: %s\", event.TargetName)\n 94→\t\t\t\te.Meta[\"username\"] = fallbackName\n 95→\t\t\t} else {\n 96→\t\t\t\te.Meta[\"username\"] = name\n 97→\t\t\t}\n 98→\t\t}\n 99→\n 100→\t\tif event.TargetEmail != \"\" {\n 101→\t\t\temail, err := store.fieldEncrypt.Decrypt(event.TargetEmail)\n 102→\t\t\tif err != nil {\n 103→\t\t\t\tcryptErr = fmt.Errorf(\"failed to decrypt email address for target id: %s\", event.TargetEmail)\n 104→\t\t\t\te.Meta[\"email\"] = fallbackEmail\n 105→\t\t\t} else {\n 106→\t\t\t\te.Meta[\"email\"] = email\n 107→\t\t\t}\n 108→\t\t}\n 109→\n 110→\t\tif event.InitiatorName != \"\" {\n 111→\t\t\tname, err := store.fieldEncrypt.Decrypt(event.InitiatorName)\n 112→\t\t\tif err != nil {\n 113→\t\t\t\tcryptErr = fmt.Errorf(\"failed to decrypt username of initiator: %s\", event.InitiatorName)\n 114→\t\t\t\te.InitiatorName = fallbackName\n 115→\t\t\t} else {\n 116→\t\t\t\te.InitiatorName = name\n 117→\t\t\t}\n 118→\t\t}\n 119→\n 120→\t\tif event.InitiatorEmail != \"\" {\n 121→\t\t\temail, err := store.fieldEncrypt.Decrypt(event.InitiatorEmail)\n 122→\t\t\tif err != nil {\n 123→\t\t\t\tcryptErr = fmt.Errorf(\"failed to decrypt email address of initiator: %s\", event.InitiatorEmail)\n 124→\t\t\t\te.InitiatorEmail = fallbackEmail\n 125→\t\t\t} else {\n 126→\t\t\t\te.InitiatorEmail = email\n 127→\t\t\t}\n 128→\t\t}\n 129→\n 130→\t\tactivityEvents = append(activityEvents, &e)\n 131→\t}\n 132→\n 133→\tif cryptErr != nil {\n 134→\t\tlog.WithContext(ctx).Warnf(\"%s\", cryptErr)\n 135→\t}\n 136→\n 137→\treturn activityEvents, nil\n 138→}\n 139→\n 140→// Get returns \"limit\" number of events from index ordered descending or ascending by a timestamp\n 141→func (store *Store) Get(ctx context.Context, accountID string, offset, limit int, descending bool) ([]*activity.Event, error) {\n 142→\tbaseQuery := store.db.Model(&activity.Event{}).\n 143→\t\tSelect(`\n 144→ events.*,\n 145→ u.name AS initiator_name,\n 146→ u.email AS initiator_email,\n 147→ t.name AS target_name,\n 148→ t.email AS target_email\n 149→ `).\n 150→\t\tJoins(`LEFT JOIN deleted_users u ON u.id = events.initiator_id`)."}]},"uuid":"1e05e1ee-86f0-4ac4-a94d-0cf8ae3a58c7","timestamp":"2026-03-02T19:04:42.352Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"a56befe92f328ad5f"},"toolUseID":"agent_msg_015MDxkSDakHg89MjVmWvshB","parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","uuid":"63e1b0b9-b641-48d9-acf5-814c7ecfc2b9","timestamp":"2026-03-02T19:04:42.366Z"} +{"parentUuid":"63e1b0b9-b641-48d9-acf5-814c7ecfc2b9","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"assistant","timestamp":"2026-03-02T19:04:45.342Z","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01Cco2KiX9ccv8CmxST5Sjov","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_011udGpp8givEVJ2asKVwmNw","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go","limit":250,"offset":180},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":5,"cache_creation_input_tokens":2020,"cache_read_input_tokens":47921,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2020},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"requestId":"req_011CYemszP4FTX2qRjxeGzsX","uuid":"39a706b5-e581-4bf8-a23f-a3467fe8a70c"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"a56befe92f328ad5f"},"toolUseID":"agent_msg_015MDxkSDakHg89MjVmWvshB","parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","uuid":"34b0563b-9e98-4431-95e4-412355a87e71","timestamp":"2026-03-02T19:04:45.343Z"} +{"parentUuid":"34b0563b-9e98-4431-95e4-412355a87e71","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"message":{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_011udGpp8givEVJ2asKVwmNw","type":"tool_result","content":" 180→\n 181→\treturn eventCopy, nil\n 182→}\n 183→\n 184→// saveDeletedUserEmailAndNameInEncrypted if the meta contains email and name then store it in encrypted way and delete\n 185→// this item from meta map\n 186→func (store *Store) saveDeletedUserEmailAndNameInEncrypted(event *activity.Event) (map[string]any, error) {\n 187→\temail, ok := event.Meta[\"email\"]\n 188→\tif !ok {\n 189→\t\treturn event.Meta, nil\n 190→\t}\n 191→\n 192→\tname, ok := event.Meta[\"name\"]\n 193→\tif !ok {\n 194→\t\treturn event.Meta, nil\n 195→\t}\n 196→\n 197→\tdeletedUser := activity.DeletedUser{\n 198→\t\tID: event.TargetID,\n 199→\t\tEncAlgo: gcmEncAlgo,\n 200→\t}\n 201→\n 202→\tencryptedEmail, err := store.fieldEncrypt.Encrypt(fmt.Sprintf(\"%s\", email))\n 203→\tif err != nil {\n 204→\t\treturn nil, err\n 205→\t}\n 206→\tdeletedUser.Email = encryptedEmail\n 207→\n 208→\tencryptedName, err := store.fieldEncrypt.Encrypt(fmt.Sprintf(\"%s\", name))\n 209→\tif err != nil {\n 210→\t\treturn nil, err\n 211→\t}\n 212→\tdeletedUser.Name = encryptedName\n 213→\n 214→\terr = store.db.Clauses(clause.OnConflict{\n 215→\t\tColumns: []clause.Column{{Name: \"id\"}},\n 216→\t\tDoUpdates: clause.AssignmentColumns([]string{\"email\", \"name\"}),\n 217→\t}).Create(deletedUser).Error\n 218→\tif err != nil {\n 219→\t\treturn nil, err\n 220→\t}\n 221→\n 222→\tif len(event.Meta) == 2 {\n 223→\t\treturn nil, nil // nolint\n 224→\t}\n 225→\tdelete(event.Meta, \"email\")\n 226→\tdelete(event.Meta, \"name\")\n 227→\treturn event.Meta, nil\n 228→}\n 229→\n 230→// Close the Store\n 231→func (store *Store) Close(_ context.Context) error {\n 232→\tif store.db != nil {\n 233→\t\tsql, err := store.db.DB()\n 234→\t\tif err != nil {\n 235→\t\t\treturn err\n 236→\t\t}\n 237→\t\treturn sql.Close()\n 238→\t}\n 239→\treturn nil\n 240→}\n 241→\n 242→func initDatabase(ctx context.Context, dataDir string) (*gorm.DB, error) {\n 243→\tvar dialector gorm.Dialector\n 244→\tvar storeEngine = types.SqliteStoreEngine\n 245→\n 246→\tif engine, ok := os.LookupEnv(storeEngineEnv); ok {\n 247→\t\tstoreEngine = types.Engine(engine)\n 248→\t}\n 249→\n 250→\tswitch storeEngine {\n 251→\tcase types.SqliteStoreEngine:\n 252→\t\tdialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB))\n 253→\tcase types.PostgresStoreEngine:\n 254→\t\tdsn, ok := os.LookupEnv(postgresDsnEnv)\n 255→\t\tif !ok {\n 256→\t\t\treturn nil, fmt.Errorf(\"%s environment variable not set\", postgresDsnEnv)\n 257→\t\t}\n 258→\t\tdialector = postgres.Open(dsn)\n 259→\tdefault:\n 260→\t\treturn nil, fmt.Errorf(\"unsupported store engine: %s\", storeEngine)\n 261→\t}\n 262→\tlog.WithContext(ctx).Infof(\"using %s as activity event store engine\", storeEngine)\n 263→\n 264→\tdb, err := gorm.Open(dialector, &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})\n 265→\tif err != nil {\n 266→\t\treturn nil, fmt.Errorf(\"open db connection: %w\", err)\n 267→\t}\n 268→\n 269→\treturn configureConnectionPool(db, storeEngine)\n 270→}\n 271→\n 272→func configureConnectionPool(db *gorm.DB, storeEngine types.Engine) (*gorm.DB, error) {\n 273→\tsqlDB, err := db.DB()\n 274→\tif err != nil {\n 275→\t\treturn nil, err\n 276→\t}\n 277→\n 278→\tconns, err := strconv.Atoi(os.Getenv(sqlMaxOpenConnsEnv))\n 279→\tif err != nil {\n 280→\t\tconns = runtime.NumCPU()\n 281→\t}\n 282→\tif storeEngine == types.SqliteStoreEngine {\n 283→\t\tconns = 1\n 284→\t}\n 285→\n 286→\tsqlDB.SetMaxOpenConns(conns)\n 287→\tsqlDB.SetMaxIdleConns(conns)\n 288→\tsqlDB.SetConnMaxLifetime(time.Hour)\n 289→\tsqlDB.SetConnMaxIdleTime(3 * time.Minute)\n 290→\n 291→\tlog.Infof(\"Set max open db connections to %d, max idle to %d, max lifetime to %v, max idle time to %v\",\n 292→\t\tconns, conns, time.Hour, 3*time.Minute)\n 293→\n 294→\treturn db, nil\n 295→}\n 296→"}]},"uuid":"76e0fbde-b994-4bca-99d2-e44dda51336b","timestamp":"2026-03-02T19:04:45.372Z"},"normalizedMessages":[],"type":"agent_progress","prompt":"","agentId":"a56befe92f328ad5f"},"toolUseID":"agent_msg_015MDxkSDakHg89MjVmWvshB","parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","uuid":"193df74a-a05a-4f39-bdbe-f3794c41a764","timestamp":"2026-03-02T19:04:45.373Z"} +{"parentUuid":"193df74a-a05a-4f39-bdbe-f3794c41a764","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Agent","command":"entire hooks claude-code post-task"},"parentToolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","toolUseID":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","timestamp":"2026-03-02T19:05:02.963Z","uuid":"c8852122-7ff3-446d-a8fd-18f84d038023"} +{"parentUuid":"6bc7f972-e2ac-407a-ac35-52badd51eaa3","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01NoNrpuMQuusEjTJ7pjBoSp","type":"tool_result","content":[{"type":"text","text":"Perfect! Now I have all the information needed. Let me compile my findings:\n\n## Summary of Configuration Analysis\n\nBased on my thorough examination of the netbird codebase, here are the findings:\n\n### 1. **StoreConfig Struct** \n**File:** `/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/config/config.go` (lines 179-182)\n\n```go\n// StoreConfig contains Store configuration\ntype StoreConfig struct {\n\tEngine types.Engine\n}\n```\n\nThe `StoreConfig` is minimal and only contains the `Engine` field which specifies the database engine type (SQLite, PostgreSQL, or MySQL).\n\n---\n\n### 2. **Config Struct with Datadir Field**\n**File:** `/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/config/config.go` (lines 36-64)\n\n```go\n// Config of the Management service\ntype Config struct {\n\tStuns []*Host\n\tTURNConfig *TURNConfig\n\tRelay *Relay\n\tSignal *Host\n\n\tDatadir string\n\tDataStoreEncryptionKey string\n\n\tHttpConfig *HttpServerConfig\n\n\tIdpManagerConfig *idp.Config\n\n\tDeviceAuthorizationFlow *DeviceAuthorizationFlow\n\n\tPKCEAuthorizationFlow *PKCEAuthorizationFlow\n\n\tStoreConfig StoreConfig\n\n\tReverseProxy ReverseProxy\n\n\t// disable default all-to-all policy\n\tDisableDefaultPolicy bool\n\n\t// EmbeddedIdP contains configuration for the embedded Dex OIDC provider.\n\t// When set, Dex will be embedded in the management server and serve requests at /oauth2/\n\tEmbeddedIdP *idp.EmbeddedIdPConfig\n}\n```\n\nThe `Datadir` field (line 43) is a string that stores the base directory path for all persistent data.\n\n---\n\n### 3. **ActivityStore Configuration and Usage**\n**File:** `/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/boot.go` (lines 79-93)\n\n```go\nfunc (s *BaseServer) EventStore() activity.Store {\n\treturn Create(s, func() activity.Store {\n\t\tintegrationMetrics, err := integrations.InitIntegrationMetrics(context.Background(), s.Metrics())\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to initialize integration metrics: %v\", err)\n\t\t}\n\n\t\teventStore, _, err := integrations.InitEventStore(context.Background(), s.Config.Datadir, s.Config.DataStoreEncryptionKey, integrationMetrics)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to initialize event store: %v\", err)\n\t\t}\n\n\t\treturn eventStore\n\t})\n}\n```\n\nThe `ActivityStore` (aka `EventStore`) is initialized by calling `integrations.InitEventStore()` which receives `s.Config.Datadir` and the encryption key. The event store implementation is in the `management/server/activity/store` package.\n\n---\n\n### 4. **Activity Store File Path Determination**\n**File:** `/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go` (lines 242-252)\n\n```go\nfunc initDatabase(ctx context.Context, dataDir string) (*gorm.DB, error) {\n\tvar dialector gorm.Dialector\n\tvar storeEngine = types.SqliteStoreEngine\n\n\tif engine, ok := os.LookupEnv(storeEngineEnv); ok {\n\t\tstoreEngine = types.Engine(engine)\n\t}\n\n\tswitch storeEngine {\n\tcase types.SqliteStoreEngine:\n\t\tdialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB))\n\tcase types.PostgresStoreEngine:\n\t\tdsn, ok := os.LookupEnv(postgresDsnEnv)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"%s environment variable not set\", postgresDsnEnv)\n\t\t}\n\t\tdialector = postgres.Open(dsn)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported store engine: %s\", storeEngine)\n\t}\n\tlog.WithContext(ctx).Infof(\"using %s as activity event store engine\", storeEngine)\n```\n\nAnd the constant is defined at line 26:\n```go\nconst (\n\t// eventSinkDB is the default name of the events database\n\teventSinkDB = \"events.db\"\n```\n\n**Key insight:** For SQLite, the activity events database file path is determined as: `filepath.Join(dataDir, \"events.db\")` (line 252). For PostgreSQL, it uses the DSN from the `NB_ACTIVITY_EVENT_POSTGRES_DSN` environment variable.\n\n---\n\n### 5. **Store.NewStore Implementation**\n**File:** `/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go` (lines 344-365)\n\n```go\n// NewStore creates a new store based on the provided engine type, data directory, and telemetry metrics\nfunc NewStore(ctx context.Context, kind types.Engine, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {\n\tkind = getStoreEngine(ctx, dataDir, kind)\n\n\tif err := checkFileStoreEngine(kind, dataDir); err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch kind {\n\tcase types.SqliteStoreEngine:\n\t\tlog.WithContext(ctx).Info(\"using SQLite store engine\")\n\t\treturn NewSqliteStore(ctx, dataDir, metrics, skipMigration)\n\tcase types.PostgresStoreEngine:\n\t\tlog.WithContext(ctx).Info(\"using Postgres store engine\")\n\t\treturn newPostgresStore(ctx, metrics, skipMigration)\n\tcase types.MysqlStoreEngine:\n\t\tlog.WithContext(ctx).Info(\"using MySQL store engine\")\n\t\treturn newMysqlStore(ctx, metrics, skipMigration)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported kind of store: %s\", kind)\n\t}\n}\n```\n\nThe `dataDir` parameter is passed to the engine-specific store constructors (e.g., `NewSqliteStore`). The store initialization is called from the `BaseServer.Store()` method in boot.go at line 62.\n\n---\n\n### 6. **Standalone Management Server Store Path Handling**\n**File:** `/Users/misha/Documents/GolandProjects/netbird/netbird/management/cmd/management.go`\n\n**EmbeddedIdP Storage Configuration** (lines 207-213):\n```go\n// Set storage defaults based on Datadir\nif cfg.EmbeddedIdP.Storage.Type == \"\" {\n\tcfg.EmbeddedIdP.Storage.Type = \"sqlite3\"\n}\nif cfg.EmbeddedIdP.Storage.Config.File == \"\" && cfg.Datadir != \"\" {\n\tcfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")\n}\n```\n\n**Datadir Creation and Validation** (lines 102-107):\n```go\nif _, err = os.Stat(config.Datadir); os.IsNotExist(err) {\n\terr = os.MkdirAll(config.Datadir, 0755)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed creating datadir: %s: %v\", config.Datadir, err)\n\t}\n}\n```\n\n**Command-line Override** (lines 176-182):\n```go\n// ApplyCommandLineOverrides applies command-line flag overrides to the config\nfunc ApplyCommandLineOverrides(cfg *nbconfig.Config) {\n\tif mgmtLetsencryptDomain != \"\" {\n\t\tcfg.HttpConfig.LetsEncryptDomain = mgmtLetsencryptDomain\n\t}\n\tif mgmtDataDir != \"\" {\n\t\tcfg.Datadir = mgmtDataDir\n\t}\n\tif certKey != \"\" && certFile != \"\" {\n\t\tcfg.HttpConfig.CertFile = certFile\n\t\tcfg.HttpConfig.CertKey = certKey\n\t}\n}\n```\n\nThe management server ensures the `Datadir` exists and creates it if necessary. The datadir can be overridden via command-line flags (`mgmtDataDir`).\n\n---\n\n## Key Takeaways:\n\n1. **StoreConfig** only contains the Engine type\n2. **Config.Datadir** is the central directory for all persistent storage\n3. **Activity events** are stored in `{Datadir}/events.db` (SQLite default) or via PostgreSQL DSN\n4. **EmbeddedIdP** stores its data in `{Datadir}/idp.db`\n5. All store operations receive `dataDir` as a parameter from the main configuration\n6. The standalone server ensures `Datadir` exists before initializing any stores"},{"type":"text","text":"agentId: a56befe92f328ad5f (for resuming to continue this agent's work if needed)\n total_tokens: 53932\ntool_uses: 16\nduration_ms: 38778 "}]}]},"uuid":"a2ff58ca-14cf-498b-86f2-4423fd6865cf","timestamp":"2026-03-02T19:05:02.962Z","toolUseResult":{"status":"completed","prompt":"In the netbird codebase at /Users/misha/Documents/GolandProjects/netbird/netbird:\n\n1. Find the `StoreConfig` struct in `management/internals/server/config/` or `management/server/` - show its fields\n2. Find the `Config` struct in `management/internals/server/config/` that has `Datadir` field - show its full definition\n3. Find how `ActivityStore` or `activityStore` is configured/used in the management config and how the activity events store file path is determined\n4. Look at `store.NewStore` to see how it uses the datadir parameter\n5. Check how the standalone management server handles store file paths (management/cmd/management.go)\n\nShow relevant code snippets with file paths and line numbers.","agentId":"a56befe92f328ad5f","content":[{"type":"text","text":"Perfect! Now I have all the information needed. Let me compile my findings:\n\n## Summary of Configuration Analysis\n\nBased on my thorough examination of the netbird codebase, here are the findings:\n\n### 1. **StoreConfig Struct** \n**File:** `/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/config/config.go` (lines 179-182)\n\n```go\n// StoreConfig contains Store configuration\ntype StoreConfig struct {\n\tEngine types.Engine\n}\n```\n\nThe `StoreConfig` is minimal and only contains the `Engine` field which specifies the database engine type (SQLite, PostgreSQL, or MySQL).\n\n---\n\n### 2. **Config Struct with Datadir Field**\n**File:** `/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/config/config.go` (lines 36-64)\n\n```go\n// Config of the Management service\ntype Config struct {\n\tStuns []*Host\n\tTURNConfig *TURNConfig\n\tRelay *Relay\n\tSignal *Host\n\n\tDatadir string\n\tDataStoreEncryptionKey string\n\n\tHttpConfig *HttpServerConfig\n\n\tIdpManagerConfig *idp.Config\n\n\tDeviceAuthorizationFlow *DeviceAuthorizationFlow\n\n\tPKCEAuthorizationFlow *PKCEAuthorizationFlow\n\n\tStoreConfig StoreConfig\n\n\tReverseProxy ReverseProxy\n\n\t// disable default all-to-all policy\n\tDisableDefaultPolicy bool\n\n\t// EmbeddedIdP contains configuration for the embedded Dex OIDC provider.\n\t// When set, Dex will be embedded in the management server and serve requests at /oauth2/\n\tEmbeddedIdP *idp.EmbeddedIdPConfig\n}\n```\n\nThe `Datadir` field (line 43) is a string that stores the base directory path for all persistent data.\n\n---\n\n### 3. **ActivityStore Configuration and Usage**\n**File:** `/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/boot.go` (lines 79-93)\n\n```go\nfunc (s *BaseServer) EventStore() activity.Store {\n\treturn Create(s, func() activity.Store {\n\t\tintegrationMetrics, err := integrations.InitIntegrationMetrics(context.Background(), s.Metrics())\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to initialize integration metrics: %v\", err)\n\t\t}\n\n\t\teventStore, _, err := integrations.InitEventStore(context.Background(), s.Config.Datadir, s.Config.DataStoreEncryptionKey, integrationMetrics)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to initialize event store: %v\", err)\n\t\t}\n\n\t\treturn eventStore\n\t})\n}\n```\n\nThe `ActivityStore` (aka `EventStore`) is initialized by calling `integrations.InitEventStore()` which receives `s.Config.Datadir` and the encryption key. The event store implementation is in the `management/server/activity/store` package.\n\n---\n\n### 4. **Activity Store File Path Determination**\n**File:** `/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go` (lines 242-252)\n\n```go\nfunc initDatabase(ctx context.Context, dataDir string) (*gorm.DB, error) {\n\tvar dialector gorm.Dialector\n\tvar storeEngine = types.SqliteStoreEngine\n\n\tif engine, ok := os.LookupEnv(storeEngineEnv); ok {\n\t\tstoreEngine = types.Engine(engine)\n\t}\n\n\tswitch storeEngine {\n\tcase types.SqliteStoreEngine:\n\t\tdialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB))\n\tcase types.PostgresStoreEngine:\n\t\tdsn, ok := os.LookupEnv(postgresDsnEnv)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"%s environment variable not set\", postgresDsnEnv)\n\t\t}\n\t\tdialector = postgres.Open(dsn)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported store engine: %s\", storeEngine)\n\t}\n\tlog.WithContext(ctx).Infof(\"using %s as activity event store engine\", storeEngine)\n```\n\nAnd the constant is defined at line 26:\n```go\nconst (\n\t// eventSinkDB is the default name of the events database\n\teventSinkDB = \"events.db\"\n```\n\n**Key insight:** For SQLite, the activity events database file path is determined as: `filepath.Join(dataDir, \"events.db\")` (line 252). For PostgreSQL, it uses the DSN from the `NB_ACTIVITY_EVENT_POSTGRES_DSN` environment variable.\n\n---\n\n### 5. **Store.NewStore Implementation**\n**File:** `/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go` (lines 344-365)\n\n```go\n// NewStore creates a new store based on the provided engine type, data directory, and telemetry metrics\nfunc NewStore(ctx context.Context, kind types.Engine, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {\n\tkind = getStoreEngine(ctx, dataDir, kind)\n\n\tif err := checkFileStoreEngine(kind, dataDir); err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch kind {\n\tcase types.SqliteStoreEngine:\n\t\tlog.WithContext(ctx).Info(\"using SQLite store engine\")\n\t\treturn NewSqliteStore(ctx, dataDir, metrics, skipMigration)\n\tcase types.PostgresStoreEngine:\n\t\tlog.WithContext(ctx).Info(\"using Postgres store engine\")\n\t\treturn newPostgresStore(ctx, metrics, skipMigration)\n\tcase types.MysqlStoreEngine:\n\t\tlog.WithContext(ctx).Info(\"using MySQL store engine\")\n\t\treturn newMysqlStore(ctx, metrics, skipMigration)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported kind of store: %s\", kind)\n\t}\n}\n```\n\nThe `dataDir` parameter is passed to the engine-specific store constructors (e.g., `NewSqliteStore`). The store initialization is called from the `BaseServer.Store()` method in boot.go at line 62.\n\n---\n\n### 6. **Standalone Management Server Store Path Handling**\n**File:** `/Users/misha/Documents/GolandProjects/netbird/netbird/management/cmd/management.go`\n\n**EmbeddedIdP Storage Configuration** (lines 207-213):\n```go\n// Set storage defaults based on Datadir\nif cfg.EmbeddedIdP.Storage.Type == \"\" {\n\tcfg.EmbeddedIdP.Storage.Type = \"sqlite3\"\n}\nif cfg.EmbeddedIdP.Storage.Config.File == \"\" && cfg.Datadir != \"\" {\n\tcfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")\n}\n```\n\n**Datadir Creation and Validation** (lines 102-107):\n```go\nif _, err = os.Stat(config.Datadir); os.IsNotExist(err) {\n\terr = os.MkdirAll(config.Datadir, 0755)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed creating datadir: %s: %v\", config.Datadir, err)\n\t}\n}\n```\n\n**Command-line Override** (lines 176-182):\n```go\n// ApplyCommandLineOverrides applies command-line flag overrides to the config\nfunc ApplyCommandLineOverrides(cfg *nbconfig.Config) {\n\tif mgmtLetsencryptDomain != \"\" {\n\t\tcfg.HttpConfig.LetsEncryptDomain = mgmtLetsencryptDomain\n\t}\n\tif mgmtDataDir != \"\" {\n\t\tcfg.Datadir = mgmtDataDir\n\t}\n\tif certKey != \"\" && certFile != \"\" {\n\t\tcfg.HttpConfig.CertFile = certFile\n\t\tcfg.HttpConfig.CertKey = certKey\n\t}\n}\n```\n\nThe management server ensures the `Datadir` exists and creates it if necessary. The datadir can be overridden via command-line flags (`mgmtDataDir`).\n\n---\n\n## Key Takeaways:\n\n1. **StoreConfig** only contains the Engine type\n2. **Config.Datadir** is the central directory for all persistent storage\n3. **Activity events** are stored in `{Datadir}/events.db` (SQLite default) or via PostgreSQL DSN\n4. **EmbeddedIdP** stores its data in `{Datadir}/idp.db`\n5. All store operations receive `dataDir` as a parameter from the main configuration\n6. The standalone server ensures `Datadir` exists before initializing any stores"}],"totalDurationMs":38778,"totalTokens":53932,"totalToolUseCount":16,"usage":{"input_tokens":4,"cache_creation_input_tokens":1760,"cache_read_input_tokens":49941,"output_tokens":2227,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":1760,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"sourceToolAssistantUUID":"6bc7f972-e2ac-407a-ac35-52badd51eaa3"} +{"parentUuid":"a2ff58ca-14cf-498b-86f2-4423fd6865cf","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_013hTsvYZH77xY928tZ9s4dS","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_012eAgY9HUbx8X2EXLFFnRuK","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/config.yaml.example"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":56,"cache_creation_input_tokens":3655,"cache_read_input_tokens":34498,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":3655},"output_tokens":18,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYemuZKV456GmSFZuGutL","type":"assistant","uuid":"00ea7f66-ebcf-4d71-9f1a-a94feca42bfb","timestamp":"2026-03-02T19:05:06.569Z"} +{"parentUuid":"00ea7f66-ebcf-4d71-9f1a-a94feca42bfb","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_012eAgY9HUbx8X2EXLFFnRuK","toolUseID":"toolu_012eAgY9HUbx8X2EXLFFnRuK","timestamp":"2026-03-02T19:05:06.598Z","uuid":"ee59d5d4-943a-4a58-ae11-737d8f36c2cb"} +{"parentUuid":"00ea7f66-ebcf-4d71-9f1a-a94feca42bfb","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_012eAgY9HUbx8X2EXLFFnRuK","type":"tool_result","content":" 1→# NetBird Combined Server Configuration\n 2→# Copy this file to config.yaml and customize for your deployment\n 3→#\n 4→# This is a Management server with optional embedded Signal, Relay, and STUN services.\n 5→# By default, all services run locally. You can use external services instead by\n 6→# setting the corresponding override fields.\n 7→#\n 8→# Architecture:\n 9→# - Management: Always runs locally (this IS the management server)\n 10→# - Signal: Local by default; set 'signalUri' to use external (disables local)\n 11→# - Relay: Local by default; set 'relays' to use external (disables local)\n 12→# - STUN: Local on port 3478 by default; set 'stuns' to use external instead\n 13→\n 14→server:\n 15→ # Main HTTP/gRPC port for all services (Management, Signal, Relay)\n 16→ listenAddress: \":443\"\n 17→\n 18→ # Public address that peers will use to connect to this server\n 19→ # Used for relay connections and management DNS domain\n 20→ # Format: protocol://hostname:port (e.g., https://server.mycompany.com:443)\n 21→ exposedAddress: \"https://server.mycompany.com:443\"\n 22→\n 23→ # STUN server ports (defaults to [3478] if not specified; set 'stuns' to use external)\n 24→ # stunPorts:\n 25→ # - 3478\n 26→\n 27→ # Metrics endpoint port\n 28→ metricsPort: 9090\n 29→\n 30→ # Healthcheck endpoint address\n 31→ healthcheckAddress: \":9000\"\n 32→\n 33→ # Logging configuration\n 34→ logLevel: \"info\" # Default log level for all components: panic, fatal, error, warn, info, debug, trace\n 35→ logFile: \"console\" # \"console\" or path to log file\n 36→\n 37→ # TLS configuration (optional)\n 38→ tls:\n 39→ certFile: \"\"\n 40→ keyFile: \"\"\n 41→ letsencrypt:\n 42→ enabled: false\n 43→ dataDir: \"\"\n 44→ domains: []\n 45→ email: \"\"\n 46→ awsRoute53: false\n 47→\n 48→ # Shared secret for relay authentication (required when running local relay)\n 49→ authSecret: \"your-secret-key-here\"\n 50→\n 51→ # Data directory for all services\n 52→ dataDir: \"/var/lib/netbird/\"\n 53→\n 54→ # ============================================================================\n 55→ # External Service Overrides (optional)\n 56→ # Use these to point to external Signal, Relay, or STUN servers instead of\n 57→ # running them locally. When set, the corresponding local service is disabled.\n 58→ # ============================================================================\n 59→\n 60→ # External STUN servers - disables local STUN server\n 61→ # stuns:\n 62→ # - uri: \"stun:stun.example.com:3478\"\n 63→ # - uri: \"stun:stun.example.com:3479\"\n 64→\n 65→ # External relay servers - disables local relay server\n 66→ # relays:\n 67→ # addresses:\n 68→ # - \"rels://relay.example.com:443\"\n 69→ # credentialsTTL: \"12h\"\n 70→ # secret: \"relay-shared-secret\"\n 71→\n 72→ # External signal server - disables local signal server\n 73→ # signalUri: \"https://signal.example.com:443\"\n 74→\n 75→ # ============================================================================\n 76→ # Management Settings\n 77→ # ============================================================================\n 78→\n 79→ # Metrics and updates\n 80→ disableAnonymousMetrics: false\n 81→ disableGeoliteUpdate: false\n 82→\n 83→ # Embedded authentication/identity provider (Dex) configuration (always enabled)\n 84→ auth:\n 85→ # OIDC issuer URL - must be publicly accessible\n 86→ issuer: \"https://example.com/oauth2\"\n 87→ localAuthDisabled: false\n 88→ signKeyRefreshEnabled: false\n 89→ # OAuth2 redirect URIs for dashboard\n 90→ dashboardRedirectURIs:\n 91→ - \"https://app.example.com/nb-auth\"\n 92→ - \"https://app.example.com/nb-silent-auth\"\n 93→ # OAuth2 redirect URIs for CLI\n 94→ cliRedirectURIs:\n 95→ - \"http://localhost:53000/\"\n 96→ # Optional initial admin user\n 97→ # owner:\n 98→ # email: \"admin@example.com\"\n 99→ # password: \"initial-password\"\n 100→\n 101→ # Store configuration\n 102→ store:\n 103→ engine: \"sqlite\" # sqlite, postgres, or mysql\n 104→ dsn: \"\" # Connection string for postgres or mysql\n 105→ encryptionKey: \"\"\n 106→\n 107→ # Activity events store configuration (optional, defaults to sqlite in dataDir)\n 108→ # activityStore:\n 109→ # engine: \"sqlite\" # sqlite or postgres\n 110→ # dsn: \"\" # Connection string for postgres\n 111→\n 112→ # Auth (embedded IdP) store configuration (optional, defaults to sqlite3 in dataDir/idp.db)\n 113→ # authStore:\n 114→ # engine: \"sqlite3\" # sqlite3 or postgres\n 115→ # dsn: \"\" # Connection string for postgres (e.g., \"host=localhost port=5432 user=postgres password=postgres dbname=netbird_idp sslmode=disable\")\n 116→\n 117→ # Reverse proxy settings (optional)\n 118→ # reverseProxy:\n 119→ # trustedHTTPProxies: []\n 120→ # trustedHTTPProxiesCount: 0\n 121→ # trustedPeers: []\n 122→"}]},"uuid":"c5ccaeaa-e855-4deb-a0c9-b9391bfdee8b","timestamp":"2026-03-02T19:05:06.597Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/config.yaml.example","content":"# NetBird Combined Server Configuration\n# Copy this file to config.yaml and customize for your deployment\n#\n# This is a Management server with optional embedded Signal, Relay, and STUN services.\n# By default, all services run locally. You can use external services instead by\n# setting the corresponding override fields.\n#\n# Architecture:\n# - Management: Always runs locally (this IS the management server)\n# - Signal: Local by default; set 'signalUri' to use external (disables local)\n# - Relay: Local by default; set 'relays' to use external (disables local)\n# - STUN: Local on port 3478 by default; set 'stuns' to use external instead\n\nserver:\n # Main HTTP/gRPC port for all services (Management, Signal, Relay)\n listenAddress: \":443\"\n\n # Public address that peers will use to connect to this server\n # Used for relay connections and management DNS domain\n # Format: protocol://hostname:port (e.g., https://server.mycompany.com:443)\n exposedAddress: \"https://server.mycompany.com:443\"\n\n # STUN server ports (defaults to [3478] if not specified; set 'stuns' to use external)\n # stunPorts:\n # - 3478\n\n # Metrics endpoint port\n metricsPort: 9090\n\n # Healthcheck endpoint address\n healthcheckAddress: \":9000\"\n\n # Logging configuration\n logLevel: \"info\" # Default log level for all components: panic, fatal, error, warn, info, debug, trace\n logFile: \"console\" # \"console\" or path to log file\n\n # TLS configuration (optional)\n tls:\n certFile: \"\"\n keyFile: \"\"\n letsencrypt:\n enabled: false\n dataDir: \"\"\n domains: []\n email: \"\"\n awsRoute53: false\n\n # Shared secret for relay authentication (required when running local relay)\n authSecret: \"your-secret-key-here\"\n\n # Data directory for all services\n dataDir: \"/var/lib/netbird/\"\n\n # ============================================================================\n # External Service Overrides (optional)\n # Use these to point to external Signal, Relay, or STUN servers instead of\n # running them locally. When set, the corresponding local service is disabled.\n # ============================================================================\n\n # External STUN servers - disables local STUN server\n # stuns:\n # - uri: \"stun:stun.example.com:3478\"\n # - uri: \"stun:stun.example.com:3479\"\n\n # External relay servers - disables local relay server\n # relays:\n # addresses:\n # - \"rels://relay.example.com:443\"\n # credentialsTTL: \"12h\"\n # secret: \"relay-shared-secret\"\n\n # External signal server - disables local signal server\n # signalUri: \"https://signal.example.com:443\"\n\n # ============================================================================\n # Management Settings\n # ============================================================================\n\n # Metrics and updates\n disableAnonymousMetrics: false\n disableGeoliteUpdate: false\n\n # Embedded authentication/identity provider (Dex) configuration (always enabled)\n auth:\n # OIDC issuer URL - must be publicly accessible\n issuer: \"https://example.com/oauth2\"\n localAuthDisabled: false\n signKeyRefreshEnabled: false\n # OAuth2 redirect URIs for dashboard\n dashboardRedirectURIs:\n - \"https://app.example.com/nb-auth\"\n - \"https://app.example.com/nb-silent-auth\"\n # OAuth2 redirect URIs for CLI\n cliRedirectURIs:\n - \"http://localhost:53000/\"\n # Optional initial admin user\n # owner:\n # email: \"admin@example.com\"\n # password: \"initial-password\"\n\n # Store configuration\n store:\n engine: \"sqlite\" # sqlite, postgres, or mysql\n dsn: \"\" # Connection string for postgres or mysql\n encryptionKey: \"\"\n\n # Activity events store configuration (optional, defaults to sqlite in dataDir)\n # activityStore:\n # engine: \"sqlite\" # sqlite or postgres\n # dsn: \"\" # Connection string for postgres\n\n # Auth (embedded IdP) store configuration (optional, defaults to sqlite3 in dataDir/idp.db)\n # authStore:\n # engine: \"sqlite3\" # sqlite3 or postgres\n # dsn: \"\" # Connection string for postgres (e.g., \"host=localhost port=5432 user=postgres password=postgres dbname=netbird_idp sslmode=disable\")\n\n # Reverse proxy settings (optional)\n # reverseProxy:\n # trustedHTTPProxies: []\n # trustedHTTPProxiesCount: 0\n # trustedPeers: []\n","numLines":122,"startLine":1,"totalLines":122}},"sourceToolAssistantUUID":"00ea7f66-ebcf-4d71-9f1a-a94feca42bfb"} +{"parentUuid":"c5ccaeaa-e855-4deb-a0c9-b9391bfdee8b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_013hTsvYZH77xY928tZ9s4dS","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01DPegABoFVgyJUhVTD1CSWf","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/root.go"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":56,"cache_creation_input_tokens":3655,"cache_read_input_tokens":34498,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":3655},"output_tokens":18,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYemuZKV456GmSFZuGutL","type":"assistant","uuid":"6a546f77-39ee-4b0b-9f73-73123ef8a398","timestamp":"2026-03-02T19:05:07.186Z"} +{"parentUuid":"6a546f77-39ee-4b0b-9f73-73123ef8a398","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01DPegABoFVgyJUhVTD1CSWf","toolUseID":"toolu_01DPegABoFVgyJUhVTD1CSWf","timestamp":"2026-03-02T19:05:07.196Z","uuid":"61807585-e1b4-48cf-b938-1119263bab94"} +{"parentUuid":"6a546f77-39ee-4b0b-9f73-73123ef8a398","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01DPegABoFVgyJUhVTD1CSWf","type":"tool_result","content":" 1→package cmd\n 2→\n 3→import (\n 4→\t\"context\"\n 5→\t\"crypto/sha256\"\n 6→\t\"crypto/tls\"\n 7→\t\"errors\"\n 8→\t\"fmt\"\n 9→\t\"net\"\n 10→\t\"net/http\"\n 11→\t\"os\"\n 12→\t\"os/signal\"\n 13→\t\"strconv\"\n 14→\t\"strings\"\n 15→\t\"sync\"\n 16→\t\"syscall\"\n 17→\t\"time\"\n 18→\n 19→\t\"github.com/coder/websocket\"\n 20→\t\"github.com/hashicorp/go-multierror\"\n 21→\tlog \"github.com/sirupsen/logrus\"\n 22→\t\"github.com/spf13/cobra\"\n 23→\t\"go.opentelemetry.io/otel/metric\"\n 24→\t\"google.golang.org/grpc\"\n 25→\n 26→\t\"github.com/netbirdio/netbird/encryption\"\n 27→\tmgmtServer \"github.com/netbirdio/netbird/management/internals/server\"\n 28→\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n 29→\t\"github.com/netbirdio/netbird/management/server/telemetry\"\n 30→\t\"github.com/netbirdio/netbird/relay/healthcheck\"\n 31→\trelayServer \"github.com/netbirdio/netbird/relay/server\"\n 32→\t\"github.com/netbirdio/netbird/relay/server/listener/ws\"\n 33→\tsharedMetrics \"github.com/netbirdio/netbird/shared/metrics\"\n 34→\t\"github.com/netbirdio/netbird/shared/relay/auth\"\n 35→\t\"github.com/netbirdio/netbird/shared/signal/proto\"\n 36→\tsignalServer \"github.com/netbirdio/netbird/signal/server\"\n 37→\t\"github.com/netbirdio/netbird/stun\"\n 38→\t\"github.com/netbirdio/netbird/util\"\n 39→\t\"github.com/netbirdio/netbird/util/wsproxy\"\n 40→\twsproxyserver \"github.com/netbirdio/netbird/util/wsproxy/server\"\n 41→)\n 42→\n 43→var (\n 44→\tconfigPath string\n 45→\tconfig *CombinedConfig\n 46→\n 47→\trootCmd = &cobra.Command{\n 48→\t\tUse: \"combined\",\n 49→\t\tShort: \"Combined Netbird server (Management + Signal + Relay + STUN)\",\n 50→\t\tLong: `Combined Netbird server for self-hosted deployments.\n 51→\n 52→All services (Management, Signal, Relay) are multiplexed on a single port.\n 53→Optional STUN server runs on separate UDP ports.\n 54→\n 55→Configuration is loaded from a YAML file specified with --config.`,\n 56→\t\tSilenceUsage: true,\n 57→\t\tSilenceErrors: true,\n 58→\t\tRunE: execute,\n 59→\t}\n 60→)\n 61→\n 62→func init() {\n 63→\trootCmd.PersistentFlags().StringVarP(&configPath, \"config\", \"c\", \"\", \"path to YAML configuration file (required)\")\n 64→\t_ = rootCmd.MarkPersistentFlagRequired(\"config\")\n 65→\n 66→\trootCmd.AddCommand(newTokenCommands())\n 67→}\n 68→\n 69→func Execute() error {\n 70→\treturn rootCmd.Execute()\n 71→}\n 72→\n 73→func waitForExitSignal() {\n 74→\tosSigs := make(chan os.Signal, 1)\n 75→\tsignal.Notify(osSigs, syscall.SIGINT, syscall.SIGTERM)\n 76→\t<-osSigs\n 77→}\n 78→\n 79→func execute(cmd *cobra.Command, _ []string) error {\n 80→\tif err := initializeConfig(); err != nil {\n 81→\t\treturn err\n 82→\t}\n 83→\n 84→\t// Management is required as the base server when signal or relay are enabled\n 85→\tif (config.Signal.Enabled || config.Relay.Enabled) && !config.Management.Enabled {\n 86→\t\treturn fmt.Errorf(\"management must be enabled when signal or relay are enabled (provides the base HTTP server)\")\n 87→\t}\n 88→\n 89→\tservers, err := createAllServers(cmd.Context(), config)\n 90→\tif err != nil {\n 91→\t\treturn err\n 92→\t}\n 93→\n 94→\t// Register services with management's gRPC server using AfterInit hook\n 95→\tsetupServerHooks(servers, config)\n 96→\n 97→\t// Start management server (this also starts the HTTP listener)\n 98→\tif servers.mgmtSrv != nil {\n 99→\t\tif err := servers.mgmtSrv.Start(cmd.Context()); err != nil {\n 100→\t\t\tcleanupSTUNListeners(servers.stunListeners)\n 101→\t\t\treturn fmt.Errorf(\"failed to start management server: %w\", err)\n 102→\t\t}\n 103→\t}\n 104→\n 105→\t// Start all other servers\n 106→\twg := sync.WaitGroup{}\n 107→\tstartServers(&wg, servers.relaySrv, servers.healthcheck, servers.stunServer, servers.metricsServer)\n 108→\n 109→\twaitForExitSignal()\n 110→\n 111→\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n 112→\tdefer cancel()\n 113→\n 114→\terr = shutdownServers(ctx, servers.relaySrv, servers.healthcheck, servers.stunServer, servers.mgmtSrv, servers.metricsServer)\n 115→\twg.Wait()\n 116→\treturn err\n 117→}\n 118→\n 119→// initializeConfig loads and validates the configuration, then initializes logging.\n 120→func initializeConfig() error {\n 121→\tvar err error\n 122→\tconfig, err = LoadConfig(configPath)\n 123→\tif err != nil {\n 124→\t\treturn fmt.Errorf(\"failed to load config: %w\", err)\n 125→\t}\n 126→\n 127→\tif err := config.Validate(); err != nil {\n 128→\t\treturn fmt.Errorf(\"invalid config: %w\", err)\n 129→\t}\n 130→\n 131→\tif err := util.InitLog(config.Server.LogLevel, config.Server.LogFile); err != nil {\n 132→\t\treturn fmt.Errorf(\"failed to initialize log: %w\", err)\n 133→\t}\n 134→\n 135→\tif dsn := config.Server.Store.DSN; dsn != \"\" {\n 136→\t\tswitch strings.ToLower(config.Server.Store.Engine) {\n 137→\t\tcase \"postgres\":\n 138→\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n 139→\t\tcase \"mysql\":\n 140→\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n 141→\t\t}\n 142→\t}\n 143→\n 144→\tif engine := config.Server.ActivityStore.Engine; engine != \"\" {\n 145→\t\tengineLower := strings.ToLower(engine)\n 146→\t\tif engineLower == \"postgres\" && config.Server.ActivityStore.DSN == \"\" {\n 147→\t\t\treturn fmt.Errorf(\"activityStore.dsn is required when activityStore.engine is postgres\")\n 148→\t\t}\n 149→\t\tos.Setenv(\"NB_ACTIVITY_EVENT_STORE_ENGINE\", engineLower)\n 150→\t\tif dsn := config.Server.ActivityStore.DSN; dsn != \"\" {\n 151→\t\t\tos.Setenv(\"NB_ACTIVITY_EVENT_POSTGRES_DSN\", dsn)\n 152→\t\t}\n 153→\t}\n 154→\n 155→\tlog.Infof(\"Starting combined NetBird server\")\n 156→\tlogConfig(config)\n 157→\tlogEnvVars()\n 158→\treturn nil\n 159→}\n 160→\n 161→// serverInstances holds all server instances created during startup.\n 162→type serverInstances struct {\n 163→\trelaySrv *relayServer.Server\n 164→\tmgmtSrv *mgmtServer.BaseServer\n 165→\tsignalSrv *signalServer.Server\n 166→\thealthcheck *healthcheck.Server\n 167→\tstunServer *stun.Server\n 168→\tstunListeners []*net.UDPConn\n 169→\tmetricsServer *sharedMetrics.Metrics\n 170→}\n 171→\n 172→// createAllServers creates all server instances based on configuration.\n 173→func createAllServers(ctx context.Context, cfg *CombinedConfig) (*serverInstances, error) {\n 174→\tmetricsServer, err := sharedMetrics.NewServer(cfg.Server.MetricsPort, \"\")\n 175→\tif err != nil {\n 176→\t\treturn nil, fmt.Errorf(\"failed to create metrics server: %w\", err)\n 177→\t}\n 178→\tservers := &serverInstances{\n 179→\t\tmetricsServer: metricsServer,\n 180→\t}\n 181→\n 182→\t_, tlsSupport, err := handleTLSConfig(cfg)\n 183→\tif err != nil {\n 184→\t\treturn nil, fmt.Errorf(\"failed to setup TLS config: %w\", err)\n 185→\t}\n 186→\n 187→\tif err := servers.createRelayServer(cfg, tlsSupport); err != nil {\n 188→\t\treturn nil, err\n 189→\t}\n 190→\n 191→\tif err := servers.createManagementServer(ctx, cfg); err != nil {\n 192→\t\treturn nil, err\n 193→\t}\n 194→\n 195→\tif err := servers.createSignalServer(ctx, cfg); err != nil {\n 196→\t\treturn nil, err\n 197→\t}\n 198→\n 199→\tif err := servers.createHealthcheckServer(cfg); err != nil {\n 200→\t\treturn nil, err\n 201→\t}\n 202→\n 203→\treturn servers, nil\n 204→}\n 205→\n 206→func (s *serverInstances) createRelayServer(cfg *CombinedConfig, tlsSupport bool) error {\n 207→\tif !cfg.Relay.Enabled {\n 208→\t\treturn nil\n 209→\t}\n 210→\n 211→\tvar err error\n 212→\ts.stunListeners, err = createSTUNListeners(cfg)\n 213→\tif err != nil {\n 214→\t\treturn err\n 215→\t}\n 216→\n 217→\thashedSecret := sha256.Sum256([]byte(cfg.Relay.AuthSecret))\n 218→\tauthenticator := auth.NewTimedHMACValidator(hashedSecret[:], 24*time.Hour)\n 219→\n 220→\trelayCfg := relayServer.Config{\n 221→\t\tMeter: s.metricsServer.Meter,\n 222→\t\tExposedAddress: cfg.Relay.ExposedAddress,\n 223→\t\tAuthValidator: authenticator,\n 224→\t\tTLSSupport: tlsSupport,\n 225→\t}\n 226→\n 227→\ts.relaySrv, err = createRelayServer(relayCfg, s.stunListeners)\n 228→\tif err != nil {\n 229→\t\treturn err\n 230→\t}\n 231→\n 232→\tlog.Infof(\"Relay server created\")\n 233→\n 234→\tif len(s.stunListeners) > 0 {\n 235→\t\ts.stunServer = stun.NewServer(s.stunListeners, cfg.Relay.Stun.LogLevel)\n 236→\t}\n 237→\n 238→\treturn nil\n 239→}\n 240→\n 241→func (s *serverInstances) createManagementServer(ctx context.Context, cfg *CombinedConfig) error {\n 242→\tif !cfg.Management.Enabled {\n 243→\t\treturn nil\n 244→\t}\n 245→\n 246→\tmgmtConfig, err := cfg.ToManagementConfig()\n 247→\tif err != nil {\n 248→\t\treturn fmt.Errorf(\"failed to create management config: %w\", err)\n 249→\t}\n 250→\n 251→\t_, portStr, portErr := net.SplitHostPort(cfg.Server.ListenAddress)\n 252→\tif portErr != nil {\n 253→\t\tportStr = \"443\"\n 254→\t}\n 255→\tmgmtPort, _ := strconv.Atoi(portStr)\n 256→\n 257→\tif err := ApplyEmbeddedIdPConfig(ctx, mgmtConfig, mgmtPort, false); err != nil {\n 258→\t\tcleanupSTUNListeners(s.stunListeners)\n 259→\t\treturn fmt.Errorf(\"failed to apply embedded IdP config: %w\", err)\n 260→\t}\n 261→\n 262→\tif err := EnsureEncryptionKey(ctx, mgmtConfig); err != nil {\n 263→\t\tcleanupSTUNListeners(s.stunListeners)\n 264→\t\treturn fmt.Errorf(\"failed to ensure encryption key: %w\", err)\n 265→\t}\n 266→\n 267→\tLogConfigInfo(mgmtConfig)\n 268→\n 269→\ts.mgmtSrv, err = createManagementServer(cfg, mgmtConfig)\n 270→\tif err != nil {\n 271→\t\tcleanupSTUNListeners(s.stunListeners)\n 272→\t\treturn fmt.Errorf(\"failed to create management server: %w\", err)\n 273→\t}\n 274→\n 275→\t// Inject externally-managed AppMetrics so management uses the shared metrics server\n 276→\tappMetrics, err := telemetry.NewAppMetricsWithMeter(ctx, s.metricsServer.Meter)\n 277→\tif err != nil {\n 278→\t\tcleanupSTUNListeners(s.stunListeners)\n 279→\t\treturn fmt.Errorf(\"failed to create management app metrics: %w\", err)\n 280→\t}\n 281→\tmgmtServer.Inject[telemetry.AppMetrics](s.mgmtSrv, appMetrics)\n 282→\n 283→\tlog.Infof(\"Management server created\")\n 284→\treturn nil\n 285→}\n 286→\n 287→func (s *serverInstances) createSignalServer(ctx context.Context, cfg *CombinedConfig) error {\n 288→\tif !cfg.Signal.Enabled {\n 289→\t\treturn nil\n 290→\t}\n 291→\n 292→\tvar err error\n 293→\ts.signalSrv, err = signalServer.NewServer(ctx, s.metricsServer.Meter, \"signal_\")\n 294→\tif err != nil {\n 295→\t\tcleanupSTUNListeners(s.stunListeners)\n 296→\t\treturn fmt.Errorf(\"failed to create signal server: %w\", err)\n 297→\t}\n 298→\n 299→\tlog.Infof(\"Signal server created\")\n 300→\treturn nil\n 301→}\n 302→\n 303→func (s *serverInstances) createHealthcheckServer(cfg *CombinedConfig) error {\n 304→\thCfg := healthcheck.Config{\n 305→\t\tListenAddress: cfg.Server.HealthcheckAddress,\n 306→\t\tServiceChecker: s.relaySrv,\n 307→\t}\n 308→\n 309→\tvar err error\n 310→\ts.healthcheck, err = createHealthCheck(hCfg, s.stunListeners)\n 311→\treturn err\n 312→}\n 313→\n 314→// setupServerHooks registers services with management's gRPC server.\n 315→func setupServerHooks(servers *serverInstances, cfg *CombinedConfig) {\n 316→\tif servers.mgmtSrv == nil {\n 317→\t\treturn\n 318→\t}\n 319→\n 320→\tservers.mgmtSrv.AfterInit(func(s *mgmtServer.BaseServer) {\n 321→\t\tgrpcSrv := s.GRPCServer()\n 322→\n 323→\t\tif servers.signalSrv != nil {\n 324→\t\t\tproto.RegisterSignalExchangeServer(grpcSrv, servers.signalSrv)\n 325→\t\t\tlog.Infof(\"Signal server registered on port %s\", cfg.Server.ListenAddress)\n 326→\t\t}\n 327→\n 328→\t\ts.SetHandlerFunc(createCombinedHandler(grpcSrv, s.APIHandler(), servers.relaySrv, servers.metricsServer.Meter, cfg))\n 329→\t\tif servers.relaySrv != nil {\n 330→\t\t\tlog.Infof(\"Relay WebSocket handler added (path: /relay)\")\n 331→\t\t}\n 332→\t})\n 333→}\n 334→\n 335→func startServers(wg *sync.WaitGroup, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, metricsServer *sharedMetrics.Metrics) {\n 336→\tif srv != nil {\n 337→\t\tinstanceURL := srv.InstanceURL()\n 338→\t\tlog.Infof(\"Relay server instance URL: %s\", instanceURL.String())\n 339→\t\tlog.Infof(\"Relay WebSocket multiplexed on management port (no separate relay listener)\")\n 340→\t}\n 341→\n 342→\twg.Add(1)\n 343→\tgo func() {\n 344→\t\tdefer wg.Done()\n 345→\t\tlog.Infof(\"running metrics server: %s%s\", metricsServer.Addr, metricsServer.Endpoint)\n 346→\t\tif err := metricsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {\n 347→\t\t\tlog.Fatalf(\"failed to start metrics server: %v\", err)\n 348→\t\t}\n 349→\t}()\n 350→\n 351→\twg.Add(1)\n 352→\tgo func() {\n 353→\t\tdefer wg.Done()\n 354→\t\tif err := httpHealthcheck.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {\n 355→\t\t\tlog.Fatalf(\"failed to start healthcheck server: %v\", err)\n 356→\t\t}\n 357→\t}()\n 358→\n 359→\tif stunServer != nil {\n 360→\t\twg.Add(1)\n 361→\t\tgo func() {\n 362→\t\t\tdefer wg.Done()\n 363→\t\t\tif err := stunServer.Listen(); err != nil {\n 364→\t\t\t\tif errors.Is(err, stun.ErrServerClosed) {\n 365→\t\t\t\t\treturn\n 366→\t\t\t\t}\n 367→\t\t\t\tlog.Errorf(\"STUN server error: %v\", err)\n 368→\t\t\t}\n 369→\t\t}()\n 370→\t}\n 371→}\n 372→\n 373→func shutdownServers(ctx context.Context, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, mgmtSrv *mgmtServer.BaseServer, metricsServer *sharedMetrics.Metrics) error {\n 374→\tvar errs error\n 375→\n 376→\tif err := httpHealthcheck.Shutdown(ctx); err != nil {\n 377→\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close healthcheck server: %w\", err))\n 378→\t}\n 379→\n 380→\tif stunServer != nil {\n 381→\t\tif err := stunServer.Shutdown(); err != nil {\n 382→\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close STUN server: %w\", err))\n 383→\t\t}\n 384→\t}\n 385→\n 386→\tif srv != nil {\n 387→\t\tif err := srv.Shutdown(ctx); err != nil {\n 388→\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close relay server: %w\", err))\n 389→\t\t}\n 390→\t}\n 391→\n 392→\tif mgmtSrv != nil {\n 393→\t\tlog.Infof(\"shutting down management and signal servers\")\n 394→\t\tif err := mgmtSrv.Stop(); err != nil {\n 395→\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close management server: %w\", err))\n 396→\t\t}\n 397→\t}\n 398→\n 399→\tif metricsServer != nil {\n 400→\t\tlog.Infof(\"shutting down metrics server\")\n 401→\t\tif err := metricsServer.Shutdown(ctx); err != nil {\n 402→\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close metrics server: %w\", err))\n 403→\t\t}\n 404→\t}\n 405→\n 406→\treturn errs\n 407→}\n 408→\n 409→func createHealthCheck(hCfg healthcheck.Config, stunListeners []*net.UDPConn) (*healthcheck.Server, error) {\n 410→\thttpHealthcheck, err := healthcheck.NewServer(hCfg)\n 411→\tif err != nil {\n 412→\t\tcleanupSTUNListeners(stunListeners)\n 413→\t\treturn nil, fmt.Errorf(\"failed to create healthcheck server: %w\", err)\n 414→\t}\n 415→\treturn httpHealthcheck, nil\n 416→}\n 417→\n 418→func createRelayServer(cfg relayServer.Config, stunListeners []*net.UDPConn) (*relayServer.Server, error) {\n 419→\tsrv, err := relayServer.NewServer(cfg)\n 420→\tif err != nil {\n 421→\t\tcleanupSTUNListeners(stunListeners)\n 422→\t\treturn nil, fmt.Errorf(\"failed to create relay server: %w\", err)\n 423→\t}\n 424→\treturn srv, nil\n 425→}\n 426→\n 427→func cleanupSTUNListeners(stunListeners []*net.UDPConn) {\n 428→\tfor _, l := range stunListeners {\n 429→\t\t_ = l.Close()\n 430→\t}\n 431→}\n 432→\n 433→func createSTUNListeners(cfg *CombinedConfig) ([]*net.UDPConn, error) {\n 434→\tvar stunListeners []*net.UDPConn\n 435→\tif cfg.Relay.Stun.Enabled {\n 436→\t\tfor _, port := range cfg.Relay.Stun.Ports {\n 437→\t\t\tlistener, err := net.ListenUDP(\"udp\", &net.UDPAddr{Port: port})\n 438→\t\t\tif err != nil {\n 439→\t\t\t\tcleanupSTUNListeners(stunListeners)\n 440→\t\t\t\treturn nil, fmt.Errorf(\"failed to create STUN listener on port %d: %w\", port, err)\n 441→\t\t\t}\n 442→\t\t\tstunListeners = append(stunListeners, listener)\n 443→\t\t\tlog.Infof(\"STUN server listening on UDP port %d\", port)\n 444→\t\t}\n 445→\t}\n 446→\treturn stunListeners, nil\n 447→}\n 448→\n 449→func handleTLSConfig(cfg *CombinedConfig) (*tls.Config, bool, error) {\n 450→\ttlsCfg := cfg.Server.TLS\n 451→\n 452→\tif tlsCfg.LetsEncrypt.AWSRoute53 {\n 453→\t\tlog.Debugf(\"using Let's Encrypt DNS resolver with Route 53 support\")\n 454→\t\tr53 := encryption.Route53TLS{\n 455→\t\t\tDataDir: tlsCfg.LetsEncrypt.DataDir,\n 456→\t\t\tEmail: tlsCfg.LetsEncrypt.Email,\n 457→\t\t\tDomains: tlsCfg.LetsEncrypt.Domains,\n 458→\t\t}\n 459→\t\ttc, err := r53.GetCertificate()\n 460→\t\tif err != nil {\n 461→\t\t\treturn nil, false, err\n 462→\t\t}\n 463→\t\treturn tc, true, nil\n 464→\t}\n 465→\n 466→\tif cfg.HasLetsEncrypt() {\n 467→\t\tlog.Infof(\"setting up TLS with Let's Encrypt\")\n 468→\t\tcertManager, err := encryption.CreateCertManager(tlsCfg.LetsEncrypt.DataDir, tlsCfg.LetsEncrypt.Domains...)\n 469→\t\tif err != nil {\n 470→\t\t\treturn nil, false, fmt.Errorf(\"failed creating LetsEncrypt cert manager: %w\", err)\n 471→\t\t}\n 472→\t\treturn certManager.TLSConfig(), true, nil\n 473→\t}\n 474→\n 475→\tif cfg.HasTLSCert() {\n 476→\t\tlog.Debugf(\"using file based TLS config\")\n 477→\t\ttc, err := encryption.LoadTLSConfig(tlsCfg.CertFile, tlsCfg.KeyFile)\n 478→\t\tif err != nil {\n 479→\t\t\treturn nil, false, err\n 480→\t\t}\n 481→\t\treturn tc, true, nil\n 482→\t}\n 483→\n 484→\treturn nil, false, nil\n 485→}\n 486→\n 487→func createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (*mgmtServer.BaseServer, error) {\n 488→\tmgmt := cfg.Management\n 489→\n 490→\tdnsDomain := mgmt.DnsDomain\n 491→\tsingleAccModeDomain := dnsDomain\n 492→\n 493→\t// Extract port from listen address\n 494→\t_, portStr, err := net.SplitHostPort(cfg.Server.ListenAddress)\n 495→\tif err != nil {\n 496→\t\t// If no port specified, assume default\n 497→\t\tportStr = \"443\"\n 498→\t}\n 499→\tmgmtPort, _ := strconv.Atoi(portStr)\n 500→\n 501→\tmgmtSrv := mgmtServer.NewServer(\n 502→\t\t&mgmtServer.Config{\n 503→\t\t\tNbConfig: mgmtConfig,\n 504→\t\t\tDNSDomain: dnsDomain,\n 505→\t\t\tMgmtSingleAccModeDomain: singleAccModeDomain,\n 506→\t\t\tMgmtPort: mgmtPort,\n 507→\t\t\tMgmtMetricsPort: cfg.Server.MetricsPort,\n 508→\t\t\tDisableMetrics: mgmt.DisableAnonymousMetrics,\n 509→\t\t\tDisableGeoliteUpdate: mgmt.DisableGeoliteUpdate,\n 510→\t\t\t// Always enable user deletion from IDP in combined server (embedded IdP is always enabled)\n 511→\t\t\tUserDeleteFromIDPEnabled: true,\n 512→\t\t},\n 513→\t)\n 514→\n 515→\treturn mgmtSrv, nil\n 516→}\n 517→\n 518→// createCombinedHandler creates an HTTP handler that multiplexes Management, Signal (via wsproxy), and Relay WebSocket traffic\n 519→func createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, relaySrv *relayServer.Server, meter metric.Meter, cfg *CombinedConfig) http.Handler {\n 520→\twsProxy := wsproxyserver.New(grpcServer, wsproxyserver.WithOTelMeter(meter))\n 521→\n 522→\tvar relayAcceptFn func(conn net.Conn)\n 523→\tif relaySrv != nil {\n 524→\t\trelayAcceptFn = relaySrv.RelayAccept()\n 525→\t}\n 526→\n 527→\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n 528→\t\tswitch {\n 529→\t\t// Native gRPC traffic (HTTP/2 with gRPC content-type)\n 530→\t\tcase r.ProtoMajor == 2 && (strings.HasPrefix(r.Header.Get(\"Content-Type\"), \"application/grpc\") ||\n 531→\t\t\tstrings.HasPrefix(r.Header.Get(\"Content-Type\"), \"application/grpc+proto\")):\n 532→\t\t\tgrpcServer.ServeHTTP(w, r)\n 533→\n 534→\t\t// WebSocket proxy for Management gRPC\n 535→\t\tcase r.URL.Path == wsproxy.ProxyPath+wsproxy.ManagementComponent:\n 536→\t\t\twsProxy.Handler().ServeHTTP(w, r)\n 537→\n 538→\t\t// WebSocket proxy for Signal gRPC\n 539→\t\tcase r.URL.Path == wsproxy.ProxyPath+wsproxy.SignalComponent:\n 540→\t\t\tif cfg.Signal.Enabled {\n 541→\t\t\t\twsProxy.Handler().ServeHTTP(w, r)\n 542→\t\t\t} else {\n 543→\t\t\t\thttp.Error(w, \"Signal service not enabled\", http.StatusNotFound)\n 544→\t\t\t}\n 545→\n 546→\t\t// Relay WebSocket\n 547→\t\tcase r.URL.Path == \"/relay\":\n 548→\t\t\tif relayAcceptFn != nil {\n 549→\t\t\t\thandleRelayWebSocket(w, r, relayAcceptFn, cfg)\n 550→\t\t\t} else {\n 551→\t\t\t\thttp.Error(w, \"Relay service not enabled\", http.StatusNotFound)\n 552→\t\t\t}\n 553→\n 554→\t\t// Management HTTP API (default)\n 555→\t\tdefault:\n 556→\t\t\thttpHandler.ServeHTTP(w, r)\n 557→\t\t}\n 558→\t})\n 559→}\n 560→\n 561→// handleRelayWebSocket handles incoming WebSocket connections for the relay service\n 562→func handleRelayWebSocket(w http.ResponseWriter, r *http.Request, acceptFn func(conn net.Conn), cfg *CombinedConfig) {\n 563→\tacceptOptions := &websocket.AcceptOptions{\n 564→\t\tOriginPatterns: []string{\"*\"},\n 565→\t}\n 566→\n 567→\twsConn, err := websocket.Accept(w, r, acceptOptions)\n 568→\tif err != nil {\n 569→\t\tlog.Errorf(\"failed to accept relay ws connection: %s\", err)\n 570→\t\treturn\n 571→\t}\n 572→\n 573→\tconnRemoteAddr := r.RemoteAddr\n 574→\tif r.Header.Get(\"X-Real-Ip\") != \"\" && r.Header.Get(\"X-Real-Port\") != \"\" {\n 575→\t\tconnRemoteAddr = net.JoinHostPort(r.Header.Get(\"X-Real-Ip\"), r.Header.Get(\"X-Real-Port\"))\n 576→\t}\n 577→\n 578→\trAddr, err := net.ResolveTCPAddr(\"tcp\", connRemoteAddr)\n 579→\tif err != nil {\n 580→\t\t_ = wsConn.Close(websocket.StatusInternalError, \"internal error\")\n 581→\t\treturn\n 582→\t}\n 583→\n 584→\tlAddr, err := net.ResolveTCPAddr(\"tcp\", cfg.Server.ListenAddress)\n 585→\tif err != nil {\n 586→\t\t_ = wsConn.Close(websocket.StatusInternalError, \"internal error\")\n 587→\t\treturn\n 588→\t}\n 589→\n 590→\tlog.Debugf(\"Relay WS client connected from: %s\", rAddr)\n 591→\n 592→\tconn := ws.NewConn(wsConn, lAddr, rAddr)\n 593→\tacceptFn(conn)\n 594→}\n 595→\n 596→// logConfig prints all configuration parameters for debugging\n 597→func logConfig(cfg *CombinedConfig) {\n 598→\tlog.Info(\"=== Configuration ===\")\n 599→\tlogServerConfig(cfg)\n 600→\tlogComponentsConfig(cfg)\n 601→\tlogRelayConfig(cfg)\n 602→\tlogManagementConfig(cfg)\n 603→\tlog.Info(\"=== End Configuration ===\")\n 604→}\n 605→\n 606→func logServerConfig(cfg *CombinedConfig) {\n 607→\tlog.Info(\"--- Server ---\")\n 608→\tlog.Infof(\" Listen address: %s\", cfg.Server.ListenAddress)\n 609→\tlog.Infof(\" Exposed address: %s\", cfg.Server.ExposedAddress)\n 610→\tlog.Infof(\" Healthcheck address: %s\", cfg.Server.HealthcheckAddress)\n 611→\tlog.Infof(\" Metrics port: %d\", cfg.Server.MetricsPort)\n 612→\tlog.Infof(\" Log level: %s\", cfg.Server.LogLevel)\n 613→\tlog.Infof(\" Data dir: %s\", cfg.Server.DataDir)\n 614→\n 615→\tswitch {\n 616→\tcase cfg.HasTLSCert():\n 617→\t\tlog.Infof(\" TLS: cert=%s, key=%s\", cfg.Server.TLS.CertFile, cfg.Server.TLS.KeyFile)\n 618→\tcase cfg.HasLetsEncrypt():\n 619→\t\tlog.Infof(\" TLS: Let's Encrypt (domains=%v)\", cfg.Server.TLS.LetsEncrypt.Domains)\n 620→\tdefault:\n 621→\t\tlog.Info(\" TLS: disabled (using reverse proxy)\")\n 622→\t}\n 623→}\n 624→\n 625→func logComponentsConfig(cfg *CombinedConfig) {\n 626→\tlog.Info(\"--- Components ---\")\n 627→\tlog.Infof(\" Management: %v (log level: %s)\", cfg.Management.Enabled, cfg.Management.LogLevel)\n 628→\tlog.Infof(\" Signal: %v (log level: %s)\", cfg.Signal.Enabled, cfg.Signal.LogLevel)\n 629→\tlog.Infof(\" Relay: %v (log level: %s)\", cfg.Relay.Enabled, cfg.Relay.LogLevel)\n 630→}\n 631→\n 632→func logRelayConfig(cfg *CombinedConfig) {\n 633→\tif !cfg.Relay.Enabled {\n 634→\t\treturn\n 635→\t}\n 636→\tlog.Info(\"--- Relay ---\")\n 637→\tlog.Infof(\" Exposed address: %s\", cfg.Relay.ExposedAddress)\n 638→\tlog.Infof(\" Auth secret: %s...\", maskSecret(cfg.Relay.AuthSecret))\n 639→\tif cfg.Relay.Stun.Enabled {\n 640→\t\tlog.Infof(\" STUN ports: %v (log level: %s)\", cfg.Relay.Stun.Ports, cfg.Relay.Stun.LogLevel)\n 641→\t} else {\n 642→\t\tlog.Info(\" STUN: disabled\")\n 643→\t}\n 644→}\n 645→\n 646→func logManagementConfig(cfg *CombinedConfig) {\n 647→\tif !cfg.Management.Enabled {\n 648→\t\treturn\n 649→\t}\n 650→\tlog.Info(\"--- Management ---\")\n 651→\tlog.Infof(\" Data dir: %s\", cfg.Management.DataDir)\n 652→\tlog.Infof(\" DNS domain: %s\", cfg.Management.DnsDomain)\n 653→\tlog.Infof(\" Store engine: %s\", cfg.Management.Store.Engine)\n 654→\tif cfg.Server.Store.DSN != \"\" {\n 655→\t\tlog.Infof(\" Store DSN: %s\", maskDSNPassword(cfg.Server.Store.DSN))\n 656→\t}\n 657→\n 658→\tlog.Info(\" Auth (embedded IdP):\")\n 659→\tlog.Infof(\" Issuer: %s\", cfg.Management.Auth.Issuer)\n 660→\tlog.Infof(\" Dashboard redirect URIs: %v\", cfg.Management.Auth.DashboardRedirectURIs)\n 661→\tlog.Infof(\" CLI redirect URIs: %v\", cfg.Management.Auth.CLIRedirectURIs)\n 662→\n 663→\tlog.Info(\" Client settings:\")\n 664→\tlog.Infof(\" Signal URI: %s\", cfg.Management.SignalURI)\n 665→\tfor _, s := range cfg.Management.Stuns {\n 666→\t\tlog.Infof(\" STUN: %s\", s.URI)\n 667→\t}\n 668→\tif len(cfg.Management.Relays.Addresses) > 0 {\n 669→\t\tlog.Infof(\" Relay addresses: %v\", cfg.Management.Relays.Addresses)\n 670→\t\tlog.Infof(\" Relay credentials TTL: %s\", cfg.Management.Relays.CredentialsTTL)\n 671→\t}\n 672→}\n 673→\n 674→// logEnvVars logs all NB_ environment variables that are currently set\n 675→func logEnvVars() {\n 676→\tlog.Info(\"=== Environment Variables ===\")\n 677→\tfound := false\n 678→\tfor _, env := range os.Environ() {\n 679→\t\tif strings.HasPrefix(env, \"NB_\") {\n 680→\t\t\tkey, _, _ := strings.Cut(env, \"=\")\n 681→\t\t\tvalue := os.Getenv(key)\n 682→\t\t\tkeyLower := strings.ToLower(key)\n 683→\t\t\tif strings.Contains(keyLower, \"secret\") || strings.Contains(keyLower, \"key\") || strings.Contains(keyLower, \"password\") {\n 684→\t\t\t\tvalue = maskSecret(value)\n 685→\t\t\t} else if strings.Contains(keyLower, \"dsn\") {\n 686→\t\t\t\tvalue = maskDSNPassword(value)\n 687→\t\t\t}\n 688→\t\t\tlog.Infof(\" %s=%s\", key, value)\n 689→\t\t\tfound = true\n 690→\t\t}\n 691→\t}\n 692→\tif !found {\n 693→\t\tlog.Info(\" (none set)\")\n 694→\t}\n 695→\tlog.Info(\"=== End Environment Variables ===\")\n 696→}\n 697→\n 698→// maskDSNPassword masks the password in a DSN string.\n 699→// Handles both key=value format (\"password=secret\") and URI format (\"user:secret@host\").\n 700→func maskDSNPassword(dsn string) string {\n 701→\t// Key=value format: \"host=localhost user=nb password=secret dbname=nb\"\n 702→\tif strings.Contains(dsn, \"password=\") {\n 703→\t\tparts := strings.Fields(dsn)\n 704→\t\tfor i, p := range parts {\n 705→\t\t\tif strings.HasPrefix(p, \"password=\") {\n 706→\t\t\t\tparts[i] = \"password=****\"\n 707→\t\t\t}\n 708→\t\t}\n 709→\t\treturn strings.Join(parts, \" \")\n 710→\t}\n 711→\n 712→\t// URI format: \"user:password@host...\"\n 713→\tif atIdx := strings.Index(dsn, \"@\"); atIdx != -1 {\n 714→\t\tprefix := dsn[:atIdx]\n 715→\t\tif colonIdx := strings.Index(prefix, \":\"); colonIdx != -1 {\n 716→\t\t\treturn prefix[:colonIdx+1] + \"****\" + dsn[atIdx:]\n 717→\t\t}\n 718→\t}\n 719→\n 720→\treturn dsn\n 721→}\n 722→\n 723→// maskSecret returns first 4 chars of secret followed by \"...\"\n 724→func maskSecret(secret string) string {\n 725→\tif len(secret) <= 4 {\n 726→\t\treturn \"****\"\n 727→\t}\n 728→\treturn secret[:4] + \"...\"\n 729→}\n 730→"}]},"uuid":"47fac430-7d79-42d9-8e5c-e0ff63097018","timestamp":"2026-03-02T19:05:07.195Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/root.go","content":"package cmd\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/coder/websocket\"\n\t\"github.com/hashicorp/go-multierror\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"go.opentelemetry.io/otel/metric\"\n\t\"google.golang.org/grpc\"\n\n\t\"github.com/netbirdio/netbird/encryption\"\n\tmgmtServer \"github.com/netbirdio/netbird/management/internals/server\"\n\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n\t\"github.com/netbirdio/netbird/management/server/telemetry\"\n\t\"github.com/netbirdio/netbird/relay/healthcheck\"\n\trelayServer \"github.com/netbirdio/netbird/relay/server\"\n\t\"github.com/netbirdio/netbird/relay/server/listener/ws\"\n\tsharedMetrics \"github.com/netbirdio/netbird/shared/metrics\"\n\t\"github.com/netbirdio/netbird/shared/relay/auth\"\n\t\"github.com/netbirdio/netbird/shared/signal/proto\"\n\tsignalServer \"github.com/netbirdio/netbird/signal/server\"\n\t\"github.com/netbirdio/netbird/stun\"\n\t\"github.com/netbirdio/netbird/util\"\n\t\"github.com/netbirdio/netbird/util/wsproxy\"\n\twsproxyserver \"github.com/netbirdio/netbird/util/wsproxy/server\"\n)\n\nvar (\n\tconfigPath string\n\tconfig *CombinedConfig\n\n\trootCmd = &cobra.Command{\n\t\tUse: \"combined\",\n\t\tShort: \"Combined Netbird server (Management + Signal + Relay + STUN)\",\n\t\tLong: `Combined Netbird server for self-hosted deployments.\n\nAll services (Management, Signal, Relay) are multiplexed on a single port.\nOptional STUN server runs on separate UDP ports.\n\nConfiguration is loaded from a YAML file specified with --config.`,\n\t\tSilenceUsage: true,\n\t\tSilenceErrors: true,\n\t\tRunE: execute,\n\t}\n)\n\nfunc init() {\n\trootCmd.PersistentFlags().StringVarP(&configPath, \"config\", \"c\", \"\", \"path to YAML configuration file (required)\")\n\t_ = rootCmd.MarkPersistentFlagRequired(\"config\")\n\n\trootCmd.AddCommand(newTokenCommands())\n}\n\nfunc Execute() error {\n\treturn rootCmd.Execute()\n}\n\nfunc waitForExitSignal() {\n\tosSigs := make(chan os.Signal, 1)\n\tsignal.Notify(osSigs, syscall.SIGINT, syscall.SIGTERM)\n\t<-osSigs\n}\n\nfunc execute(cmd *cobra.Command, _ []string) error {\n\tif err := initializeConfig(); err != nil {\n\t\treturn err\n\t}\n\n\t// Management is required as the base server when signal or relay are enabled\n\tif (config.Signal.Enabled || config.Relay.Enabled) && !config.Management.Enabled {\n\t\treturn fmt.Errorf(\"management must be enabled when signal or relay are enabled (provides the base HTTP server)\")\n\t}\n\n\tservers, err := createAllServers(cmd.Context(), config)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Register services with management's gRPC server using AfterInit hook\n\tsetupServerHooks(servers, config)\n\n\t// Start management server (this also starts the HTTP listener)\n\tif servers.mgmtSrv != nil {\n\t\tif err := servers.mgmtSrv.Start(cmd.Context()); err != nil {\n\t\t\tcleanupSTUNListeners(servers.stunListeners)\n\t\t\treturn fmt.Errorf(\"failed to start management server: %w\", err)\n\t\t}\n\t}\n\n\t// Start all other servers\n\twg := sync.WaitGroup{}\n\tstartServers(&wg, servers.relaySrv, servers.healthcheck, servers.stunServer, servers.metricsServer)\n\n\twaitForExitSignal()\n\n\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\tdefer cancel()\n\n\terr = shutdownServers(ctx, servers.relaySrv, servers.healthcheck, servers.stunServer, servers.mgmtSrv, servers.metricsServer)\n\twg.Wait()\n\treturn err\n}\n\n// initializeConfig loads and validates the configuration, then initializes logging.\nfunc initializeConfig() error {\n\tvar err error\n\tconfig, err = LoadConfig(configPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to load config: %w\", err)\n\t}\n\n\tif err := config.Validate(); err != nil {\n\t\treturn fmt.Errorf(\"invalid config: %w\", err)\n\t}\n\n\tif err := util.InitLog(config.Server.LogLevel, config.Server.LogFile); err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize log: %w\", err)\n\t}\n\n\tif dsn := config.Server.Store.DSN; dsn != \"\" {\n\t\tswitch strings.ToLower(config.Server.Store.Engine) {\n\t\tcase \"postgres\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n\t\tcase \"mysql\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n\t\t}\n\t}\n\n\tif engine := config.Server.ActivityStore.Engine; engine != \"\" {\n\t\tengineLower := strings.ToLower(engine)\n\t\tif engineLower == \"postgres\" && config.Server.ActivityStore.DSN == \"\" {\n\t\t\treturn fmt.Errorf(\"activityStore.dsn is required when activityStore.engine is postgres\")\n\t\t}\n\t\tos.Setenv(\"NB_ACTIVITY_EVENT_STORE_ENGINE\", engineLower)\n\t\tif dsn := config.Server.ActivityStore.DSN; dsn != \"\" {\n\t\t\tos.Setenv(\"NB_ACTIVITY_EVENT_POSTGRES_DSN\", dsn)\n\t\t}\n\t}\n\n\tlog.Infof(\"Starting combined NetBird server\")\n\tlogConfig(config)\n\tlogEnvVars()\n\treturn nil\n}\n\n// serverInstances holds all server instances created during startup.\ntype serverInstances struct {\n\trelaySrv *relayServer.Server\n\tmgmtSrv *mgmtServer.BaseServer\n\tsignalSrv *signalServer.Server\n\thealthcheck *healthcheck.Server\n\tstunServer *stun.Server\n\tstunListeners []*net.UDPConn\n\tmetricsServer *sharedMetrics.Metrics\n}\n\n// createAllServers creates all server instances based on configuration.\nfunc createAllServers(ctx context.Context, cfg *CombinedConfig) (*serverInstances, error) {\n\tmetricsServer, err := sharedMetrics.NewServer(cfg.Server.MetricsPort, \"\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create metrics server: %w\", err)\n\t}\n\tservers := &serverInstances{\n\t\tmetricsServer: metricsServer,\n\t}\n\n\t_, tlsSupport, err := handleTLSConfig(cfg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to setup TLS config: %w\", err)\n\t}\n\n\tif err := servers.createRelayServer(cfg, tlsSupport); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := servers.createManagementServer(ctx, cfg); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := servers.createSignalServer(ctx, cfg); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := servers.createHealthcheckServer(cfg); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn servers, nil\n}\n\nfunc (s *serverInstances) createRelayServer(cfg *CombinedConfig, tlsSupport bool) error {\n\tif !cfg.Relay.Enabled {\n\t\treturn nil\n\t}\n\n\tvar err error\n\ts.stunListeners, err = createSTUNListeners(cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thashedSecret := sha256.Sum256([]byte(cfg.Relay.AuthSecret))\n\tauthenticator := auth.NewTimedHMACValidator(hashedSecret[:], 24*time.Hour)\n\n\trelayCfg := relayServer.Config{\n\t\tMeter: s.metricsServer.Meter,\n\t\tExposedAddress: cfg.Relay.ExposedAddress,\n\t\tAuthValidator: authenticator,\n\t\tTLSSupport: tlsSupport,\n\t}\n\n\ts.relaySrv, err = createRelayServer(relayCfg, s.stunListeners)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlog.Infof(\"Relay server created\")\n\n\tif len(s.stunListeners) > 0 {\n\t\ts.stunServer = stun.NewServer(s.stunListeners, cfg.Relay.Stun.LogLevel)\n\t}\n\n\treturn nil\n}\n\nfunc (s *serverInstances) createManagementServer(ctx context.Context, cfg *CombinedConfig) error {\n\tif !cfg.Management.Enabled {\n\t\treturn nil\n\t}\n\n\tmgmtConfig, err := cfg.ToManagementConfig()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create management config: %w\", err)\n\t}\n\n\t_, portStr, portErr := net.SplitHostPort(cfg.Server.ListenAddress)\n\tif portErr != nil {\n\t\tportStr = \"443\"\n\t}\n\tmgmtPort, _ := strconv.Atoi(portStr)\n\n\tif err := ApplyEmbeddedIdPConfig(ctx, mgmtConfig, mgmtPort, false); err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to apply embedded IdP config: %w\", err)\n\t}\n\n\tif err := EnsureEncryptionKey(ctx, mgmtConfig); err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to ensure encryption key: %w\", err)\n\t}\n\n\tLogConfigInfo(mgmtConfig)\n\n\ts.mgmtSrv, err = createManagementServer(cfg, mgmtConfig)\n\tif err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to create management server: %w\", err)\n\t}\n\n\t// Inject externally-managed AppMetrics so management uses the shared metrics server\n\tappMetrics, err := telemetry.NewAppMetricsWithMeter(ctx, s.metricsServer.Meter)\n\tif err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to create management app metrics: %w\", err)\n\t}\n\tmgmtServer.Inject[telemetry.AppMetrics](s.mgmtSrv, appMetrics)\n\n\tlog.Infof(\"Management server created\")\n\treturn nil\n}\n\nfunc (s *serverInstances) createSignalServer(ctx context.Context, cfg *CombinedConfig) error {\n\tif !cfg.Signal.Enabled {\n\t\treturn nil\n\t}\n\n\tvar err error\n\ts.signalSrv, err = signalServer.NewServer(ctx, s.metricsServer.Meter, \"signal_\")\n\tif err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to create signal server: %w\", err)\n\t}\n\n\tlog.Infof(\"Signal server created\")\n\treturn nil\n}\n\nfunc (s *serverInstances) createHealthcheckServer(cfg *CombinedConfig) error {\n\thCfg := healthcheck.Config{\n\t\tListenAddress: cfg.Server.HealthcheckAddress,\n\t\tServiceChecker: s.relaySrv,\n\t}\n\n\tvar err error\n\ts.healthcheck, err = createHealthCheck(hCfg, s.stunListeners)\n\treturn err\n}\n\n// setupServerHooks registers services with management's gRPC server.\nfunc setupServerHooks(servers *serverInstances, cfg *CombinedConfig) {\n\tif servers.mgmtSrv == nil {\n\t\treturn\n\t}\n\n\tservers.mgmtSrv.AfterInit(func(s *mgmtServer.BaseServer) {\n\t\tgrpcSrv := s.GRPCServer()\n\n\t\tif servers.signalSrv != nil {\n\t\t\tproto.RegisterSignalExchangeServer(grpcSrv, servers.signalSrv)\n\t\t\tlog.Infof(\"Signal server registered on port %s\", cfg.Server.ListenAddress)\n\t\t}\n\n\t\ts.SetHandlerFunc(createCombinedHandler(grpcSrv, s.APIHandler(), servers.relaySrv, servers.metricsServer.Meter, cfg))\n\t\tif servers.relaySrv != nil {\n\t\t\tlog.Infof(\"Relay WebSocket handler added (path: /relay)\")\n\t\t}\n\t})\n}\n\nfunc startServers(wg *sync.WaitGroup, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, metricsServer *sharedMetrics.Metrics) {\n\tif srv != nil {\n\t\tinstanceURL := srv.InstanceURL()\n\t\tlog.Infof(\"Relay server instance URL: %s\", instanceURL.String())\n\t\tlog.Infof(\"Relay WebSocket multiplexed on management port (no separate relay listener)\")\n\t}\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tlog.Infof(\"running metrics server: %s%s\", metricsServer.Addr, metricsServer.Endpoint)\n\t\tif err := metricsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {\n\t\t\tlog.Fatalf(\"failed to start metrics server: %v\", err)\n\t\t}\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tif err := httpHealthcheck.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {\n\t\t\tlog.Fatalf(\"failed to start healthcheck server: %v\", err)\n\t\t}\n\t}()\n\n\tif stunServer != nil {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tif err := stunServer.Listen(); err != nil {\n\t\t\t\tif errors.Is(err, stun.ErrServerClosed) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlog.Errorf(\"STUN server error: %v\", err)\n\t\t\t}\n\t\t}()\n\t}\n}\n\nfunc shutdownServers(ctx context.Context, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, mgmtSrv *mgmtServer.BaseServer, metricsServer *sharedMetrics.Metrics) error {\n\tvar errs error\n\n\tif err := httpHealthcheck.Shutdown(ctx); err != nil {\n\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close healthcheck server: %w\", err))\n\t}\n\n\tif stunServer != nil {\n\t\tif err := stunServer.Shutdown(); err != nil {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close STUN server: %w\", err))\n\t\t}\n\t}\n\n\tif srv != nil {\n\t\tif err := srv.Shutdown(ctx); err != nil {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close relay server: %w\", err))\n\t\t}\n\t}\n\n\tif mgmtSrv != nil {\n\t\tlog.Infof(\"shutting down management and signal servers\")\n\t\tif err := mgmtSrv.Stop(); err != nil {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close management server: %w\", err))\n\t\t}\n\t}\n\n\tif metricsServer != nil {\n\t\tlog.Infof(\"shutting down metrics server\")\n\t\tif err := metricsServer.Shutdown(ctx); err != nil {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close metrics server: %w\", err))\n\t\t}\n\t}\n\n\treturn errs\n}\n\nfunc createHealthCheck(hCfg healthcheck.Config, stunListeners []*net.UDPConn) (*healthcheck.Server, error) {\n\thttpHealthcheck, err := healthcheck.NewServer(hCfg)\n\tif err != nil {\n\t\tcleanupSTUNListeners(stunListeners)\n\t\treturn nil, fmt.Errorf(\"failed to create healthcheck server: %w\", err)\n\t}\n\treturn httpHealthcheck, nil\n}\n\nfunc createRelayServer(cfg relayServer.Config, stunListeners []*net.UDPConn) (*relayServer.Server, error) {\n\tsrv, err := relayServer.NewServer(cfg)\n\tif err != nil {\n\t\tcleanupSTUNListeners(stunListeners)\n\t\treturn nil, fmt.Errorf(\"failed to create relay server: %w\", err)\n\t}\n\treturn srv, nil\n}\n\nfunc cleanupSTUNListeners(stunListeners []*net.UDPConn) {\n\tfor _, l := range stunListeners {\n\t\t_ = l.Close()\n\t}\n}\n\nfunc createSTUNListeners(cfg *CombinedConfig) ([]*net.UDPConn, error) {\n\tvar stunListeners []*net.UDPConn\n\tif cfg.Relay.Stun.Enabled {\n\t\tfor _, port := range cfg.Relay.Stun.Ports {\n\t\t\tlistener, err := net.ListenUDP(\"udp\", &net.UDPAddr{Port: port})\n\t\t\tif err != nil {\n\t\t\t\tcleanupSTUNListeners(stunListeners)\n\t\t\t\treturn nil, fmt.Errorf(\"failed to create STUN listener on port %d: %w\", port, err)\n\t\t\t}\n\t\t\tstunListeners = append(stunListeners, listener)\n\t\t\tlog.Infof(\"STUN server listening on UDP port %d\", port)\n\t\t}\n\t}\n\treturn stunListeners, nil\n}\n\nfunc handleTLSConfig(cfg *CombinedConfig) (*tls.Config, bool, error) {\n\ttlsCfg := cfg.Server.TLS\n\n\tif tlsCfg.LetsEncrypt.AWSRoute53 {\n\t\tlog.Debugf(\"using Let's Encrypt DNS resolver with Route 53 support\")\n\t\tr53 := encryption.Route53TLS{\n\t\t\tDataDir: tlsCfg.LetsEncrypt.DataDir,\n\t\t\tEmail: tlsCfg.LetsEncrypt.Email,\n\t\t\tDomains: tlsCfg.LetsEncrypt.Domains,\n\t\t}\n\t\ttc, err := r53.GetCertificate()\n\t\tif err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\t\treturn tc, true, nil\n\t}\n\n\tif cfg.HasLetsEncrypt() {\n\t\tlog.Infof(\"setting up TLS with Let's Encrypt\")\n\t\tcertManager, err := encryption.CreateCertManager(tlsCfg.LetsEncrypt.DataDir, tlsCfg.LetsEncrypt.Domains...)\n\t\tif err != nil {\n\t\t\treturn nil, false, fmt.Errorf(\"failed creating LetsEncrypt cert manager: %w\", err)\n\t\t}\n\t\treturn certManager.TLSConfig(), true, nil\n\t}\n\n\tif cfg.HasTLSCert() {\n\t\tlog.Debugf(\"using file based TLS config\")\n\t\ttc, err := encryption.LoadTLSConfig(tlsCfg.CertFile, tlsCfg.KeyFile)\n\t\tif err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\t\treturn tc, true, nil\n\t}\n\n\treturn nil, false, nil\n}\n\nfunc createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (*mgmtServer.BaseServer, error) {\n\tmgmt := cfg.Management\n\n\tdnsDomain := mgmt.DnsDomain\n\tsingleAccModeDomain := dnsDomain\n\n\t// Extract port from listen address\n\t_, portStr, err := net.SplitHostPort(cfg.Server.ListenAddress)\n\tif err != nil {\n\t\t// If no port specified, assume default\n\t\tportStr = \"443\"\n\t}\n\tmgmtPort, _ := strconv.Atoi(portStr)\n\n\tmgmtSrv := mgmtServer.NewServer(\n\t\t&mgmtServer.Config{\n\t\t\tNbConfig: mgmtConfig,\n\t\t\tDNSDomain: dnsDomain,\n\t\t\tMgmtSingleAccModeDomain: singleAccModeDomain,\n\t\t\tMgmtPort: mgmtPort,\n\t\t\tMgmtMetricsPort: cfg.Server.MetricsPort,\n\t\t\tDisableMetrics: mgmt.DisableAnonymousMetrics,\n\t\t\tDisableGeoliteUpdate: mgmt.DisableGeoliteUpdate,\n\t\t\t// Always enable user deletion from IDP in combined server (embedded IdP is always enabled)\n\t\t\tUserDeleteFromIDPEnabled: true,\n\t\t},\n\t)\n\n\treturn mgmtSrv, nil\n}\n\n// createCombinedHandler creates an HTTP handler that multiplexes Management, Signal (via wsproxy), and Relay WebSocket traffic\nfunc createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, relaySrv *relayServer.Server, meter metric.Meter, cfg *CombinedConfig) http.Handler {\n\twsProxy := wsproxyserver.New(grpcServer, wsproxyserver.WithOTelMeter(meter))\n\n\tvar relayAcceptFn func(conn net.Conn)\n\tif relaySrv != nil {\n\t\trelayAcceptFn = relaySrv.RelayAccept()\n\t}\n\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch {\n\t\t// Native gRPC traffic (HTTP/2 with gRPC content-type)\n\t\tcase r.ProtoMajor == 2 && (strings.HasPrefix(r.Header.Get(\"Content-Type\"), \"application/grpc\") ||\n\t\t\tstrings.HasPrefix(r.Header.Get(\"Content-Type\"), \"application/grpc+proto\")):\n\t\t\tgrpcServer.ServeHTTP(w, r)\n\n\t\t// WebSocket proxy for Management gRPC\n\t\tcase r.URL.Path == wsproxy.ProxyPath+wsproxy.ManagementComponent:\n\t\t\twsProxy.Handler().ServeHTTP(w, r)\n\n\t\t// WebSocket proxy for Signal gRPC\n\t\tcase r.URL.Path == wsproxy.ProxyPath+wsproxy.SignalComponent:\n\t\t\tif cfg.Signal.Enabled {\n\t\t\t\twsProxy.Handler().ServeHTTP(w, r)\n\t\t\t} else {\n\t\t\t\thttp.Error(w, \"Signal service not enabled\", http.StatusNotFound)\n\t\t\t}\n\n\t\t// Relay WebSocket\n\t\tcase r.URL.Path == \"/relay\":\n\t\t\tif relayAcceptFn != nil {\n\t\t\t\thandleRelayWebSocket(w, r, relayAcceptFn, cfg)\n\t\t\t} else {\n\t\t\t\thttp.Error(w, \"Relay service not enabled\", http.StatusNotFound)\n\t\t\t}\n\n\t\t// Management HTTP API (default)\n\t\tdefault:\n\t\t\thttpHandler.ServeHTTP(w, r)\n\t\t}\n\t})\n}\n\n// handleRelayWebSocket handles incoming WebSocket connections for the relay service\nfunc handleRelayWebSocket(w http.ResponseWriter, r *http.Request, acceptFn func(conn net.Conn), cfg *CombinedConfig) {\n\tacceptOptions := &websocket.AcceptOptions{\n\t\tOriginPatterns: []string{\"*\"},\n\t}\n\n\twsConn, err := websocket.Accept(w, r, acceptOptions)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to accept relay ws connection: %s\", err)\n\t\treturn\n\t}\n\n\tconnRemoteAddr := r.RemoteAddr\n\tif r.Header.Get(\"X-Real-Ip\") != \"\" && r.Header.Get(\"X-Real-Port\") != \"\" {\n\t\tconnRemoteAddr = net.JoinHostPort(r.Header.Get(\"X-Real-Ip\"), r.Header.Get(\"X-Real-Port\"))\n\t}\n\n\trAddr, err := net.ResolveTCPAddr(\"tcp\", connRemoteAddr)\n\tif err != nil {\n\t\t_ = wsConn.Close(websocket.StatusInternalError, \"internal error\")\n\t\treturn\n\t}\n\n\tlAddr, err := net.ResolveTCPAddr(\"tcp\", cfg.Server.ListenAddress)\n\tif err != nil {\n\t\t_ = wsConn.Close(websocket.StatusInternalError, \"internal error\")\n\t\treturn\n\t}\n\n\tlog.Debugf(\"Relay WS client connected from: %s\", rAddr)\n\n\tconn := ws.NewConn(wsConn, lAddr, rAddr)\n\tacceptFn(conn)\n}\n\n// logConfig prints all configuration parameters for debugging\nfunc logConfig(cfg *CombinedConfig) {\n\tlog.Info(\"=== Configuration ===\")\n\tlogServerConfig(cfg)\n\tlogComponentsConfig(cfg)\n\tlogRelayConfig(cfg)\n\tlogManagementConfig(cfg)\n\tlog.Info(\"=== End Configuration ===\")\n}\n\nfunc logServerConfig(cfg *CombinedConfig) {\n\tlog.Info(\"--- Server ---\")\n\tlog.Infof(\" Listen address: %s\", cfg.Server.ListenAddress)\n\tlog.Infof(\" Exposed address: %s\", cfg.Server.ExposedAddress)\n\tlog.Infof(\" Healthcheck address: %s\", cfg.Server.HealthcheckAddress)\n\tlog.Infof(\" Metrics port: %d\", cfg.Server.MetricsPort)\n\tlog.Infof(\" Log level: %s\", cfg.Server.LogLevel)\n\tlog.Infof(\" Data dir: %s\", cfg.Server.DataDir)\n\n\tswitch {\n\tcase cfg.HasTLSCert():\n\t\tlog.Infof(\" TLS: cert=%s, key=%s\", cfg.Server.TLS.CertFile, cfg.Server.TLS.KeyFile)\n\tcase cfg.HasLetsEncrypt():\n\t\tlog.Infof(\" TLS: Let's Encrypt (domains=%v)\", cfg.Server.TLS.LetsEncrypt.Domains)\n\tdefault:\n\t\tlog.Info(\" TLS: disabled (using reverse proxy)\")\n\t}\n}\n\nfunc logComponentsConfig(cfg *CombinedConfig) {\n\tlog.Info(\"--- Components ---\")\n\tlog.Infof(\" Management: %v (log level: %s)\", cfg.Management.Enabled, cfg.Management.LogLevel)\n\tlog.Infof(\" Signal: %v (log level: %s)\", cfg.Signal.Enabled, cfg.Signal.LogLevel)\n\tlog.Infof(\" Relay: %v (log level: %s)\", cfg.Relay.Enabled, cfg.Relay.LogLevel)\n}\n\nfunc logRelayConfig(cfg *CombinedConfig) {\n\tif !cfg.Relay.Enabled {\n\t\treturn\n\t}\n\tlog.Info(\"--- Relay ---\")\n\tlog.Infof(\" Exposed address: %s\", cfg.Relay.ExposedAddress)\n\tlog.Infof(\" Auth secret: %s...\", maskSecret(cfg.Relay.AuthSecret))\n\tif cfg.Relay.Stun.Enabled {\n\t\tlog.Infof(\" STUN ports: %v (log level: %s)\", cfg.Relay.Stun.Ports, cfg.Relay.Stun.LogLevel)\n\t} else {\n\t\tlog.Info(\" STUN: disabled\")\n\t}\n}\n\nfunc logManagementConfig(cfg *CombinedConfig) {\n\tif !cfg.Management.Enabled {\n\t\treturn\n\t}\n\tlog.Info(\"--- Management ---\")\n\tlog.Infof(\" Data dir: %s\", cfg.Management.DataDir)\n\tlog.Infof(\" DNS domain: %s\", cfg.Management.DnsDomain)\n\tlog.Infof(\" Store engine: %s\", cfg.Management.Store.Engine)\n\tif cfg.Server.Store.DSN != \"\" {\n\t\tlog.Infof(\" Store DSN: %s\", maskDSNPassword(cfg.Server.Store.DSN))\n\t}\n\n\tlog.Info(\" Auth (embedded IdP):\")\n\tlog.Infof(\" Issuer: %s\", cfg.Management.Auth.Issuer)\n\tlog.Infof(\" Dashboard redirect URIs: %v\", cfg.Management.Auth.DashboardRedirectURIs)\n\tlog.Infof(\" CLI redirect URIs: %v\", cfg.Management.Auth.CLIRedirectURIs)\n\n\tlog.Info(\" Client settings:\")\n\tlog.Infof(\" Signal URI: %s\", cfg.Management.SignalURI)\n\tfor _, s := range cfg.Management.Stuns {\n\t\tlog.Infof(\" STUN: %s\", s.URI)\n\t}\n\tif len(cfg.Management.Relays.Addresses) > 0 {\n\t\tlog.Infof(\" Relay addresses: %v\", cfg.Management.Relays.Addresses)\n\t\tlog.Infof(\" Relay credentials TTL: %s\", cfg.Management.Relays.CredentialsTTL)\n\t}\n}\n\n// logEnvVars logs all NB_ environment variables that are currently set\nfunc logEnvVars() {\n\tlog.Info(\"=== Environment Variables ===\")\n\tfound := false\n\tfor _, env := range os.Environ() {\n\t\tif strings.HasPrefix(env, \"NB_\") {\n\t\t\tkey, _, _ := strings.Cut(env, \"=\")\n\t\t\tvalue := os.Getenv(key)\n\t\t\tkeyLower := strings.ToLower(key)\n\t\t\tif strings.Contains(keyLower, \"secret\") || strings.Contains(keyLower, \"key\") || strings.Contains(keyLower, \"password\") {\n\t\t\t\tvalue = maskSecret(value)\n\t\t\t} else if strings.Contains(keyLower, \"dsn\") {\n\t\t\t\tvalue = maskDSNPassword(value)\n\t\t\t}\n\t\t\tlog.Infof(\" %s=%s\", key, value)\n\t\t\tfound = true\n\t\t}\n\t}\n\tif !found {\n\t\tlog.Info(\" (none set)\")\n\t}\n\tlog.Info(\"=== End Environment Variables ===\")\n}\n\n// maskDSNPassword masks the password in a DSN string.\n// Handles both key=value format (\"password=secret\") and URI format (\"user:secret@host\").\nfunc maskDSNPassword(dsn string) string {\n\t// Key=value format: \"host=localhost user=nb password=secret dbname=nb\"\n\tif strings.Contains(dsn, \"password=\") {\n\t\tparts := strings.Fields(dsn)\n\t\tfor i, p := range parts {\n\t\t\tif strings.HasPrefix(p, \"password=\") {\n\t\t\t\tparts[i] = \"password=****\"\n\t\t\t}\n\t\t}\n\t\treturn strings.Join(parts, \" \")\n\t}\n\n\t// URI format: \"user:password@host...\"\n\tif atIdx := strings.Index(dsn, \"@\"); atIdx != -1 {\n\t\tprefix := dsn[:atIdx]\n\t\tif colonIdx := strings.Index(prefix, \":\"); colonIdx != -1 {\n\t\t\treturn prefix[:colonIdx+1] + \"****\" + dsn[atIdx:]\n\t\t}\n\t}\n\n\treturn dsn\n}\n\n// maskSecret returns first 4 chars of secret followed by \"...\"\nfunc maskSecret(secret string) string {\n\tif len(secret) <= 4 {\n\t\treturn \"****\"\n\t}\n\treturn secret[:4] + \"...\"\n}\n","numLines":730,"startLine":1,"totalLines":730}},"sourceToolAssistantUUID":"6a546f77-39ee-4b0b-9f73-73123ef8a398"} +{"parentUuid":"47fac430-7d79-42d9-8e5c-e0ff63097018","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01JHEbi9vGwF27BojtTvg457","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"Now I have a clear picture. Let me also check how `NewSqliteStore` uses the `dataDir` parameter and how the activity store determines its file path.\n\nLet me look at:\n1. `NewSqliteStore` to see how it constructs the sqlite file path from `dataDir`\n2. The activity store initialization to see how it constructs its file path from `dataDir`\n\nThis will help me understand what needs to change.","signature":"EqsECkYICxgCKkBadOUuaxe1DwurOnHByhucHKLz+07NpyJHw9Dy8CC9cK1kxq1Kh6btpEnJBim0iJxE/3pTahUrpjyh1WRm1jQpEgyHzYOhb3bLJg6ZB/EaDLTXQjTbSMuOqCHkTSIwPM8wB/gi52Kb+blBNoNxFxXa7BISm8Dl/G+2wUPdipIr+LlloN706WLYzjftosQMKpIDblRtGB6LkW+766ZHdmOhXKmkntDeztIp4vWkN9cWlp9+DY89aGy7ypR8wnUf7tOqnJzmqvH+XGNQukMS0MfrxVBe73+HDnzDRIxLhZh98m4PcwznEPdRE6J9R7fG6/vXCe3Kh4XAbSpEBaF5D9/C2rrTtBPUC4iG364T8scS7KFDgtw2B6q8iSrgUE8xfmwopO/T73TiqXGohvvslW7S6EF2Xc8oifituF1s9NHRoCDIy/kPgUCg/mnuCrw2CfzLtwWZj2AOwj8hR6kVOoYl2/Gw3DHr0PwNPKSVQGvvAuqrEEzoxSdMbsjgju8Ezoow4NNUdiKuahQU64yEJJh/xKwspjWsltLXRcnfyKPytjGURxzL4NS6faHitmJ7Vy3vEOVpx2ege1EasxrGQMDf9roqVd2pUhes0gT+MrASUBbNBJvhv7RIpnvmCLDkmfG9lyDIz5go3FMriHqkHzAUDBtitpDJ2y5w30TPoc4J29pYAAU7VLi4cOEdDcU9uNP5JLfY52cpeZsNg8/zITUtT5qIGAE="}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":12474,"cache_read_input_tokens":38153,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":12474},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYemuqv8My5K8SMtb5Rmm","type":"assistant","uuid":"98de45ed-140f-4702-9de1-548f391e3ea0","timestamp":"2026-03-02T19:05:12.875Z"} +{"parentUuid":"98de45ed-140f-4702-9de1-548f391e3ea0","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01JHEbi9vGwF27BojtTvg457","type":"message","role":"assistant","content":[{"type":"text","text":"Let me check how the SQLite stores construct their file paths from `dataDir`."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":12474,"cache_read_input_tokens":38153,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":12474},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYemuqv8My5K8SMtb5Rmm","type":"assistant","uuid":"9572857a-d68a-4905-9f10-643d1bdd0e5b","timestamp":"2026-03-02T19:05:13.832Z"} +{"parentUuid":"9572857a-d68a-4905-9f10-643d1bdd0e5b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01JHEbi9vGwF27BojtTvg457","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01ETSGBih8AiNpg2PArSRj7Q","name":"Grep","input":{"pattern":"func NewSqliteStore","output_mode":"content","-A":15},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":12474,"cache_read_input_tokens":38153,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":12474},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYemuqv8My5K8SMtb5Rmm","type":"assistant","uuid":"13f9e321-bdca-40b9-bd7b-425df3b4ab7e","timestamp":"2026-03-02T19:05:14.704Z"} +{"parentUuid":"13f9e321-bdca-40b9-bd7b-425df3b4ab7e","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Grep","command":"callback"},"parentToolUseID":"toolu_01ETSGBih8AiNpg2PArSRj7Q","toolUseID":"toolu_01ETSGBih8AiNpg2PArSRj7Q","timestamp":"2026-03-02T19:05:15.263Z","uuid":"e19d51c3-39ee-421a-95db-78f4b1594e70"} +{"parentUuid":"13f9e321-bdca-40b9-bd7b-425df3b4ab7e","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01ETSGBih8AiNpg2PArSRj7Q","type":"tool_result","content":"management/server/store/sql_store.go:2730:func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\nmanagement/server/store/sql_store.go-2731-\tstoreStr := fmt.Sprintf(\"%s?cache=shared\", storeSqliteFileName)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2732-\tif runtime.GOOS == \"windows\" {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2733-\t\t// Vo avoid `The process cannot access the file because it is being used by another process` on Windows\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2734-\t\tstoreStr = storeSqliteFileName\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2735-\t}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2736-\nmanagement/server/store/sql_store.go-2737-\tfile := filepath.Join(dataDir, storeStr)\nmanagement/server/store/sql_store.go-2738-\tdb, err := gorm.Open(sqlite.Open(file), getGormConfig())\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2739-\tif err != nil {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2740-\t\treturn nil, err\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2741-\t}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2742-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2743-\treturn NewSqlStore(ctx, db, types.SqliteStoreEngine, metrics, skipMigration)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2744-}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2745-\n--\nmanagement/server/store/sql_store.go:2825:func NewSqliteStoreFromFileStore(ctx context.Context, fileStore *FileStore, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\nmanagement/server/store/sql_store.go-2826-\tstore, err := NewSqliteStore(ctx, dataDir, metrics, skipMigration)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2827-\tif err != nil {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2828-\t\treturn nil, err\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2829-\t}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2830-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2831-\terr = store.SaveInstallationID(ctx, fileStore.InstallationID)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2832-\tif err != nil {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2833-\t\treturn nil, err\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2834-\t}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2835-\nmanagement/server/store/sql_store.go-2836-\tfor _, account := range fileStore.GetAllAccounts(ctx) {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2837-\t\t_, err = account.GetGroupAll()\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2838-\t\tif err != nil {\nmanagement/server/store/sql_store.go-2839-\t\t\tif err := account.AddAllGroup(false); err != nil {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2840-\t\t\t\treturn nil, err\n--\nmanagement/server/geolocation/store.go:48:func NewSqliteStore(ctx context.Context, dataDir string, dbPath string) (*SqliteStore, error) {\nmanagement/server/geolocation/store.go-49-\tfile := filepath.Join(dataDir, dbPath)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-50-\nmanagement/server/geolocation/store.go-51-\tdb, err := connectDB(ctx, file)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-52-\tif err != nil {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-53-\t\treturn nil, err\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-54-\t}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-55-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-56-\treturn &SqliteStore{\nmanagement/server/geolocation/store.go-57-\t\tdb: db,\nmanagement/server/geolocation/store.go-58-\t\tfilePath: file,\nmanagement/server/geolocation/store.go-59-\t\tmux: sync.RWMutex{},\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-60-\t}, nil\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-61-}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-62-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-63-// GetAllCountries returns a list of all countries in the store."}]},"uuid":"d5950ece-7c22-4430-8d8f-d93737d2ae5e","timestamp":"2026-03-02T19:05:15.262Z","toolUseResult":{"mode":"content","numFiles":0,"filenames":[],"content":"management/server/store/sql_store.go:2730:func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\nmanagement/server/store/sql_store.go-2731-\tstoreStr := fmt.Sprintf(\"%s?cache=shared\", storeSqliteFileName)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2732-\tif runtime.GOOS == \"windows\" {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2733-\t\t// Vo avoid `The process cannot access the file because it is being used by another process` on Windows\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2734-\t\tstoreStr = storeSqliteFileName\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2735-\t}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2736-\nmanagement/server/store/sql_store.go-2737-\tfile := filepath.Join(dataDir, storeStr)\nmanagement/server/store/sql_store.go-2738-\tdb, err := gorm.Open(sqlite.Open(file), getGormConfig())\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2739-\tif err != nil {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2740-\t\treturn nil, err\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2741-\t}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2742-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2743-\treturn NewSqlStore(ctx, db, types.SqliteStoreEngine, metrics, skipMigration)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2744-}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2745-\n--\nmanagement/server/store/sql_store.go:2825:func NewSqliteStoreFromFileStore(ctx context.Context, fileStore *FileStore, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\nmanagement/server/store/sql_store.go-2826-\tstore, err := NewSqliteStore(ctx, dataDir, metrics, skipMigration)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2827-\tif err != nil {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2828-\t\treturn nil, err\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2829-\t}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2830-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2831-\terr = store.SaveInstallationID(ctx, fileStore.InstallationID)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2832-\tif err != nil {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2833-\t\treturn nil, err\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2834-\t}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2835-\nmanagement/server/store/sql_store.go-2836-\tfor _, account := range fileStore.GetAllAccounts(ctx) {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2837-\t\t_, err = account.GetGroupAll()\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2838-\t\tif err != nil {\nmanagement/server/store/sql_store.go-2839-\t\t\tif err := account.AddAllGroup(false); err != nil {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-2840-\t\t\t\treturn nil, err\n--\nmanagement/server/geolocation/store.go:48:func NewSqliteStore(ctx context.Context, dataDir string, dbPath string) (*SqliteStore, error) {\nmanagement/server/geolocation/store.go-49-\tfile := filepath.Join(dataDir, dbPath)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-50-\nmanagement/server/geolocation/store.go-51-\tdb, err := connectDB(ctx, file)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-52-\tif err != nil {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-53-\t\treturn nil, err\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-54-\t}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-55-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-56-\treturn &SqliteStore{\nmanagement/server/geolocation/store.go-57-\t\tdb: db,\nmanagement/server/geolocation/store.go-58-\t\tfilePath: file,\nmanagement/server/geolocation/store.go-59-\t\tmux: sync.RWMutex{},\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-60-\t}, nil\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-61-}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-62-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/geolocation/store.go-63-// GetAllCountries returns a list of all countries in the store.","numLines":50},"sourceToolAssistantUUID":"13f9e321-bdca-40b9-bd7b-425df3b4ab7e"} +{"parentUuid":"d5950ece-7c22-4430-8d8f-d93737d2ae5e","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01JHEbi9vGwF27BojtTvg457","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_014hsAGnbV5CGaY7KWths8Et","name":"Grep","input":{"pattern":"eventSinkDB|events\\.db","path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity","output_mode":"content","-B":2,"-A":5},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":12474,"cache_read_input_tokens":38153,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":12474},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYemuqv8My5K8SMtb5Rmm","type":"assistant","uuid":"bac926a9-a6e3-4d53-8c5e-690d93f5d2d6","timestamp":"2026-03-02T19:05:16.457Z"} +{"parentUuid":"bac926a9-a6e3-4d53-8c5e-690d93f5d2d6","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Grep","command":"callback"},"parentToolUseID":"toolu_014hsAGnbV5CGaY7KWths8Et","toolUseID":"toolu_014hsAGnbV5CGaY7KWths8Et","timestamp":"2026-03-02T19:05:16.848Z","uuid":"a036502e-5e83-47da-8ad9-f527ef401a44"} +{"parentUuid":"bac926a9-a6e3-4d53-8c5e-690d93f5d2d6","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_014hsAGnbV5CGaY7KWths8Et","type":"tool_result","content":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-23-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-24-const (\nmanagement/server/activity/store/sql_store.go:25:\t// eventSinkDB is the default name of the events database\nmanagement/server/activity/store/sql_store.go:26:\teventSinkDB = \"events.db\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-27-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-28-\tfallbackName = \"unknown\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-29-\tfallbackEmail = \"unknown@unknown.com\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-30-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-31-\tgcmEncAlgo = \"GCM\"\n--\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-250-\tswitch storeEngine {\nmanagement/server/activity/store/sql_store.go-251-\tcase types.SqliteStoreEngine:\nmanagement/server/activity/store/sql_store.go:252:\t\tdialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB))\nmanagement/server/activity/store/sql_store.go-253-\tcase types.PostgresStoreEngine:\nmanagement/server/activity/store/sql_store.go-254-\t\tdsn, ok := os.LookupEnv(postgresDsnEnv)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-255-\t\tif !ok {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-256-\t\t\treturn nil, fmt.Errorf(\"%s environment variable not set\", postgresDsnEnv)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-257-\t\t}"}]},"uuid":"f7989d10-7ab4-43b5-af5d-0cd96be2416d","timestamp":"2026-03-02T19:05:16.847Z","toolUseResult":{"mode":"content","numFiles":0,"filenames":[],"content":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-23-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-24-const (\nmanagement/server/activity/store/sql_store.go:25:\t// eventSinkDB is the default name of the events database\nmanagement/server/activity/store/sql_store.go:26:\teventSinkDB = \"events.db\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-27-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-28-\tfallbackName = \"unknown\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-29-\tfallbackEmail = \"unknown@unknown.com\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-30-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-31-\tgcmEncAlgo = \"GCM\"\n--\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-250-\tswitch storeEngine {\nmanagement/server/activity/store/sql_store.go-251-\tcase types.SqliteStoreEngine:\nmanagement/server/activity/store/sql_store.go:252:\t\tdialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB))\nmanagement/server/activity/store/sql_store.go-253-\tcase types.PostgresStoreEngine:\nmanagement/server/activity/store/sql_store.go-254-\t\tdsn, ok := os.LookupEnv(postgresDsnEnv)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-255-\t\tif !ok {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-256-\t\t\treturn nil, fmt.Errorf(\"%s environment variable not set\", postgresDsnEnv)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go-257-\t\t}","numLines":18},"sourceToolAssistantUUID":"bac926a9-a6e3-4d53-8c5e-690d93f5d2d6"} +{"parentUuid":"f7989d10-7ab4-43b5-af5d-0cd96be2416d","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01JHEbi9vGwF27BojtTvg457","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_0147qhov2fRqLBXcByGAz7BN","name":"Grep","input":{"pattern":"storeFileName|store\\.db","path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store","output_mode":"content","-B":2,"-A":5},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":12474,"cache_read_input_tokens":38153,"output_tokens":499,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":12474,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYemuqv8My5K8SMtb5Rmm","type":"assistant","uuid":"e04afb98-79b6-4711-adc2-9f4392c1e981","timestamp":"2026-03-02T19:05:17.419Z"} +{"parentUuid":"e04afb98-79b6-4711-adc2-9f4392c1e981","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Grep","command":"callback"},"parentToolUseID":"toolu_0147qhov2fRqLBXcByGAz7BN","toolUseID":"toolu_0147qhov2fRqLBXcByGAz7BN","timestamp":"2026-03-02T19:05:17.809Z","uuid":"ebc6a98b-0bb0-4afb-aab8-30d4286f224f"} +{"parentUuid":"e04afb98-79b6-4711-adc2-9f4392c1e981","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_0147qhov2fRqLBXcByGAz7BN","type":"tool_result","content":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-20-)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-21-\nmanagement/server/store/file_store.go:22:// storeFileName Store file name. Stored in the datadir\nmanagement/server/store/file_store.go:23:const storeFileName = \"store.json\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-24-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-25-// FileStore represents an account storage backed by a file persisted to disk\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-26-type FileStore struct {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-27-\tAccounts map[string]*types.Account\nmanagement/server/store/file_store.go-28-\tSetupKeyID2AccountID map[string]string `json:\"-\"`\n--\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-44-// NewFileStore restores a store from the file located in the datadir\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-45-func NewFileStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics) (*FileStore, error) {\nmanagement/server/store/file_store.go:46:\tfs, err := restore(ctx, filepath.Join(dataDir, storeFileName))\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-47-\tif err != nil {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-48-\t\treturn nil, err\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-49-\t}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-50-\tfs.metrics = metrics\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-51-\treturn fs, nil\n--\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-324-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-325-\t\t\t// Migrate if it is the first run with a JSON file existing and no SQLite file present\nmanagement/server/store/store.go:326:\t\t\tjsonStoreFile := filepath.Join(dataDir, storeFileName)\nmanagement/server/store/store.go-327-\t\t\tsqliteStoreFile := filepath.Join(dataDir, storeSqliteFileName)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-328-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-329-\t\t\tif util.FileExists(jsonStoreFile) && !util.FileExists(sqliteStoreFile) {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-330-\t\t\t\tlog.WithContext(ctx).Warnf(\"unsupported store engine specified, but found %s. Automatically migrating to SQLite.\", jsonStoreFile)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-331-\n--\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-367-func checkFileStoreEngine(kind types.Engine, dataDir string) error {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-368-\tif kind == types.FileStoreEngine {\nmanagement/server/store/store.go:369:\t\tstoreFile := filepath.Join(dataDir, storeFileName)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-370-\t\tif util.FileExists(storeFile) {\nmanagement/server/store/store.go-371-\t\t\treturn fmt.Errorf(\"%s is not supported. Please refer to the documentation for migrating to SQLite: \"+\nmanagement/server/store/store.go-372-\t\t\t\t\"https://docs.netbird.io/selfhosted/sqlite-store#migrating-from-json-store-to-sq-lite-store\", types.FileStoreEngine)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-373-\t\t}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-374-\t}\n--\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-781-// MigrateFileStoreToSqlite migrates the file store to the SQLite store.\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-782-func MigrateFileStoreToSqlite(ctx context.Context, dataDir string) error {\nmanagement/server/store/store.go:783:\tfileStorePath := path.Join(dataDir, storeFileName)\nmanagement/server/store/store.go-784-\tif _, err := os.Stat(fileStorePath); errors.Is(err, os.ErrNotExist) {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-785-\t\treturn fmt.Errorf(\"%s doesn't exist, couldn't continue the operation\", fileStorePath)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-786-\t}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-787-\nmanagement/server/store/store.go-788-\tsqlStorePath := path.Join(dataDir, storeSqliteFileName)\n--\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-48-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-49-const (\nmanagement/server/store/sql_store.go:50:\tstoreSqliteFileName = \"store.db\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-51-\tidQueryCondition = \"id = ?\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-52-\tkeyQueryCondition = \"key = ?\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-53-\tmysqlKeyQueryCondition = \"`key` = ?\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-54-\taccountAndIDQueryCondition = \"account_id = ? and id = ?\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-55-\taccountAndPeerIDQueryCondition = \"account_id = ? and peer_id = ?\""}]},"uuid":"e5db4d4d-2333-4143-9f7a-789152be6a56","timestamp":"2026-03-02T19:05:17.808Z","toolUseResult":{"mode":"content","numFiles":0,"filenames":[],"content":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-20-)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-21-\nmanagement/server/store/file_store.go:22:// storeFileName Store file name. Stored in the datadir\nmanagement/server/store/file_store.go:23:const storeFileName = \"store.json\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-24-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-25-// FileStore represents an account storage backed by a file persisted to disk\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-26-type FileStore struct {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-27-\tAccounts map[string]*types.Account\nmanagement/server/store/file_store.go-28-\tSetupKeyID2AccountID map[string]string `json:\"-\"`\n--\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-44-// NewFileStore restores a store from the file located in the datadir\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-45-func NewFileStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics) (*FileStore, error) {\nmanagement/server/store/file_store.go:46:\tfs, err := restore(ctx, filepath.Join(dataDir, storeFileName))\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-47-\tif err != nil {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-48-\t\treturn nil, err\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-49-\t}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-50-\tfs.metrics = metrics\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/file_store.go-51-\treturn fs, nil\n--\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-324-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-325-\t\t\t// Migrate if it is the first run with a JSON file existing and no SQLite file present\nmanagement/server/store/store.go:326:\t\t\tjsonStoreFile := filepath.Join(dataDir, storeFileName)\nmanagement/server/store/store.go-327-\t\t\tsqliteStoreFile := filepath.Join(dataDir, storeSqliteFileName)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-328-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-329-\t\t\tif util.FileExists(jsonStoreFile) && !util.FileExists(sqliteStoreFile) {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-330-\t\t\t\tlog.WithContext(ctx).Warnf(\"unsupported store engine specified, but found %s. Automatically migrating to SQLite.\", jsonStoreFile)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-331-\n--\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-367-func checkFileStoreEngine(kind types.Engine, dataDir string) error {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-368-\tif kind == types.FileStoreEngine {\nmanagement/server/store/store.go:369:\t\tstoreFile := filepath.Join(dataDir, storeFileName)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-370-\t\tif util.FileExists(storeFile) {\nmanagement/server/store/store.go-371-\t\t\treturn fmt.Errorf(\"%s is not supported. Please refer to the documentation for migrating to SQLite: \"+\nmanagement/server/store/store.go-372-\t\t\t\t\"https://docs.netbird.io/selfhosted/sqlite-store#migrating-from-json-store-to-sq-lite-store\", types.FileStoreEngine)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-373-\t\t}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-374-\t}\n--\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-781-// MigrateFileStoreToSqlite migrates the file store to the SQLite store.\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-782-func MigrateFileStoreToSqlite(ctx context.Context, dataDir string) error {\nmanagement/server/store/store.go:783:\tfileStorePath := path.Join(dataDir, storeFileName)\nmanagement/server/store/store.go-784-\tif _, err := os.Stat(fileStorePath); errors.Is(err, os.ErrNotExist) {\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-785-\t\treturn fmt.Errorf(\"%s doesn't exist, couldn't continue the operation\", fileStorePath)\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-786-\t}\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go-787-\nmanagement/server/store/store.go-788-\tsqlStorePath := path.Join(dataDir, storeSqliteFileName)\n--\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-48-\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-49-const (\nmanagement/server/store/sql_store.go:50:\tstoreSqliteFileName = \"store.db\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-51-\tidQueryCondition = \"id = ?\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-52-\tkeyQueryCondition = \"key = ?\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-53-\tmysqlKeyQueryCondition = \"`key` = ?\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-54-\taccountAndIDQueryCondition = \"account_id = ? and id = ?\"\n/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go-55-\taccountAndPeerIDQueryCondition = \"account_id = ? and peer_id = ?\"","numLines":54},"sourceToolAssistantUUID":"e04afb98-79b6-4711-adc2-9f4392c1e981"} +{"parentUuid":"e5db4d4d-2333-4143-9f7a-789152be6a56","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_013NnRTfVdP7WHgS5CNPQxdE","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"Now I have a clear picture of what needs to be done:\n\n**Current state:**\n- `StoreConfig` has: `Engine`, `EncryptionKey`, `DSN`\n- For SQLite, the main store uses `filepath.Join(dataDir, \"store.db\")` - hardcoded filename\n- For SQLite, the activity store uses `filepath.Join(dataDir, \"events.db\")` - hardcoded filename via env vars\n- For SQLite, the auth store uses `path.Join(mgmt.DataDir, \"idp.db\")` - hardcoded filename\n\n**What the user wants:**\n\n\n- Add a `File` field to `StoreConfig` so users can specify custom SQLite file paths for each store type\n\nI need to add this field to `StoreConfig` and then update how `store.NewStore` and the other store constructors receive the file path, since they currently get `dataDir` separately and hardcode the filenames internally.\n\nLooking at the downstream components, the main store, activity store, and auth store all construct their file paths from `dataDir` at initialization time. I could either add a file parameter to each constructor or use environment variables to configure custom paths for the activity and auth stores. _EVENT_POSTGRES_DSN`. For SQLite, hardcodes `filepath.Join(dataDir, eventSinkDB)`.\n\n3. **Auth store** (`idp.db`): Uses `idp.EmbeddedStorageTypeConfig.File` which can be set directly.\n\nThe cleanest approach that only touches `combined/` would be:\n- For the main store: Add `File` to the `StoreConfig` in combined, and pass it through to `nbconfig.StoreConfig`. But `nbconfig.StoreConfig` doesn't have such a field. So I'd need to modify `nbconfig.StoreConfig` too... OR I could modify the `nbconfig.Config.Datadir` to point to the directory containing the file, but that would affect other stores.\n\nActually, the user wants to add a `file` field to the combined config's `StoreConfig` that allows specifying a custom SQLite file path for the main store, auth store, and activity store. Let me trace through how each one currently constructs its path. The main store calls `store.NewStore` with a datadir and constructs the path internally, the auth store has its own config structure, and the activity store does similar path construction. The `nbconfig.StoreConfig` is pretty minimal right now—just an `Engine` field—so I'd need to extend it to support custom file paths. I could add a `SqliteFile` field to it. But that would require changes outside `combined/`.\n\nActually, re-reading the user's request more carefully - they said \"in combined/\". So they want the changes scoped to the combined server configuration. Let me think about how to implement this with minimal changes outside combined/:\n\nFor the combined config's `StoreConfig`, I'd add a `File` field for the SQLite file path, then in the combined config processing, I need to get this into the management server. The management server's `NewStore` takes `dataDir`, so I could set `dataDir` to the directory containing the custom file, but that gets messy.\n\nLooking at this differently - `store.NewSqliteStore` hardcodes the filename, so I have a few options: modify it to accept an optional file path (but that changes management code), use environment variables like the activity store does, add a field to `nbconfig.StoreConfig` and have `store.NewStore` use it, or adjust the `dataDir` approach. Since the combined server already sets environment variables for DSN configuration, that pattern seems like the cleanest fit here. for the combined server might be:\n\nFor main store, I could:\n- If a custom file is specified, modify the dataDir to be the directory of that file and set a new env var... no, that's too convoluted.\n\nLet me look at what changes are actually needed downstream. The user said \"in combined/\" which suggests they want changes scoped there. But to properly support custom SQLite file paths, we'll likely need some changes in the store packages too.\n\nActually, wait. Let me re-read. For authStore, the `file` field already exists in `AuthStorageConfig`:\n```go\ntype AuthStorageConfig struct {\n Type string `yaml:\"type\"`\n File string `yaml:\"file\"`\n}\n```\n\nAnd it's used in `buildEmbeddedIdPConfig`:\n```go\nauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n```\n\nSo the user's asking about adding a `file` field to the `StoreConfig` type itself for the combined config's store, authStore, and activityStore. For authStore, I can use this new field in `buildEmbeddedIdPConfig` instead of always joining with the data directory. For activityStore, the file path is currently set as `filepath.Join(dataDir, \"events.db\")` in the activity store initialization, so I'd need to pass the custom file path there too if provided.\n\nFor the main store, it's `filepath.Join(dataDir, \"store.db\")` in `NewSqliteStore`. I'd need to handle that similarly.\n\nThe activity store doesn't currently have an env var for custom file paths, so I'll need to add one or modify how it's initialized.\n\nLooking at the options, I think the cleanest approach is to add a `File` field to the combined `StoreConfig` that can be passed through to both stores—using it directly in `buildEmbeddedIdPConfig` for the auth store, and adding a `SqliteFile` field to the main store config to support custom paths there too.","signature":"EuI+CkYICxgCKkC2rr5qmJYKQ2oIr7yK4LluqIKmB56Jl1axQpCf4tuVvvWN5y+VVkygEUqnSY5dP7FW0eVtKT5AX2XtOlV3cK00EgyviTEoP0WOtpD9XVsaDM3/u4+/S3G/aCyQtiIwK6o4HgzPnMMpPnS41X6rsCXvlI+HvablKEJXLqT4WsrfQxN0Br0pvlKNbwYdnrsDKsk9Aci0dGoq98tRD5UDUEeCX/NBfgAdfawD+EYs8ZJtSUfn1hFZJA9HkNXGIoWqYEeyj7BaUcnypDgk5y35P6700hpgaUGhLTpGIKpky0h6lY8UlABTjk1uxbRpXDjogZ3jCqAtukUaVnEY08O2/ILdPhS8ujKkM3vbCz3kxpXfQ0RiGX1eonqkR06SKAsFSIEiYyW2rESSfEvvNG9G/NeneC6DAbNCyk/uKlx21UBrJS6pQRjPf8k50nbUAh1QjONu4puS2CzdA3tg1uUTz59WJU8DXZDwZ4NGCQXfv3MsMfpleeLR1kEcJGU1ZsEuc66Ism8+86gYIkkYUPTomkt4+dfw6nYgvXwg8YCG1Qmua7Q2paJd60e0EGAjWln/LTIreovwjA9BtlaqCqaoJdFtI8w67jQvLxGxm3oD3shs3m8Sf82NfG79YH8QgyGwVqZL549bZAb0CxNtQ0DseX/XrvpfJQYs8WInRcJzkxEuRuu8JnViDGlZwg8CNTLplR1WhnZ2xfAMRiwDr4QM5RqPnePdXpyyTjUjxNB+CohYcyp01OP+sLIm8HmnnJnlsycCnAtpvtTOiC3tyyXrdOtuGN88T3ily8vizQUNtfRhRMuRo7G+6bX67tzOt7O8aifKKD11Kifm5IJeJYeuApqTTIyrK6xSNrYO4QW/fEMaOOzPISaloQEUoa+EJW0zkK/9rhdjuwbQKRAhAgCSwg4oJIaahhQKmlD0kLLurXxEDEplmgeZ6gc1QTqEDYW1/95qP0nn7HzQyTwpx5JcUppfRj20C/+9hqOp7n8sVCGMnv3pIVJTwj0m/ha9xt/9+QikjO9bZMjnTZECVhZ+KPxJ73Dh5006THY9OQMsGdWMSpZWbWjNkifb1JO93bTy03iUZDGtvhsxwU5BuhU+cB4+b4cvr+YISs3TizvPr0MLy7R80rEGSqgklYOj/XenyaI8QUJPtDMrq07XcSMmlWF6eCvIqn2h+gkQNtNx4hzBnLEaHZrTr+Y2CNgzV6/gh5hU3u+wcwiQnryE8jlG1hljQnmv0wWWOYXEkOj+Adx2Vp8B+kWn21+61ZwZIBFuJnc4QdB8FDzMR42O3aafpi7FFaucLnrQDaOiAJehr5OmutkiKt6/V1wjcLM4f9tgx2hPYp/vKI6i9p8Is5PV2z9lbYbHPacC4sd5zcv5b8kWsPxXryLxP65nc1jUrFWhysxp/v8aByqvaPXHXpMChnZi1jLttUmLqWKLmNikygS8fG+Nu6FYpUeQ/3lK2T7CRmpPX5TzAosX6SeF/gqxdIZNy4/6rPWawHrw4squRJBgYwcTJ44A7J1b40RqOwUsc8eBB1DM8Z+5ogwkbRqcqZHb9rzcIp7URxH1WCurbq7bYajxRFCIlOcWHT3kplUXOet3hi97Xe6vATGHkDmQpxUVq+smDUbISfciVDLOodccjfxiuxrj+McNRQDyGTcW0jeaJr9AfZwFTaDN2cXf+vOaxeJGg8see4hlYSz4nO5aGx4a1qvJ72yS6kft7p2DkE/QwHT7mNYeU/1QYY1ZMb/PlXEoF6pvhncOvpCsBkQ2Bd0v0+/TYd0g9JF8pPNYHD4FD6iQblVBoYTQ2ZqTmHrMT+rc1SJhotZLeDpBRXgYkqPETuHL6Zva2MbESwCLTGT1VBn+MBEuRlD2s/5tt6vx9pHsPhySuA1n77H9ZEf9KHF05IYfjdla/4Zy3G8qA39/CEWEj2BV5EMJaLw52htKNLqnFwBcXXAfGeUNLjUQRrOmx2kU3QOtbnqtbU43hcUyg4zRimSFH/DU6LvHryejMef0WAm3+KTu+tHMloGlxvWGtEReSL1Liyi5+DhWET0EnE7HbfipoBM3WkIIReQEZP3cTSrU/NDRJ6Caj+4uN+HoodMCABlT3RIkN3brXKnt0awk+j46cgECVQywrWwfvNSEXIBc6afyKmP2KHp660iIOE6ReCVVpX2FGSIytdN41JhXOxEdI/Fxjip+FbWYeqUjXEpoWS4DT7x94/tl0xkYianaiaNO+L8lgYAOYSpm+jmGLExcTFMiSTjuLpB66AshkFIMsPNGRIAvuKL+1GjDMb8uyzu1oJda8xJ58/67KvRdjCaJUAW3a67alcMNSNjydGng51kPtnncZRaqJ7rUjeCCM+pdUmRl9MfmLj8vpQLhK84xhlWU10Oo/4fVmrrDC2Cs4QLYvNHK87bYmP+A2ovBYD3XLHIvIURUnhkubbEOdGXHv6xGwAcGF3Z23PVW8Jd5cCIpXdtZPwW59wr2m8IjSgnsHj6efflNxGcj/jrYKzyMaCGplci/8lhj5wh339O22bq9pX0Rw6VMli/ngYMi1Eindyhb7Lahmms/e58lCoMHN/fi0VQZfcd8rckFX+mKKODUa6pW0o1rLZBELvh9sEozZKJa1y62o50cbFav87W8SyrLHmEYgdMQDoaB7PY7H4wOm0N8gurjzaTRyD4+bCOJnkQBuXTHJMFbWSr4Ne9DKlxrl0VeI/J2pJdiMvFx0UgZe17VlxYP1MkU3OVQXPD39QW5JY/lFuqXooPHH2CzS6XpBDW48qmkrG0iCQUtG4u9TrIDXFYltKAC7hlr++UYRj6PuCQyKY+hRs6hzibV0RiDS4DEJEbqw5j+QSz2OcRVCP/VEJz1BrcmeLymdvvpvCKEJbzEx7a6UrQKeOavgVmW/k6/J5QvU3wW7jQsE0v/3lv6zng5lu7qj/wVqY9XuuR6KLb+8+Phl5cZA1tW2K04YhddoIBq0L3TArGJY3iGZNZ7DmJuPY/UBmyHGJhUuJRi3khKDO2PHwEI7eTbT3s9M0jojByefVizRHkpBABpoO5GTZ9XMTOY8I6jVTg6EVZhOnLY1G2Z0aUMYbl+32IUDUmj/PudhJ+jZdVfmrYeqL2layuQ+naiVsNmWgmsjd+pXlUey2wXs8rggXdPFgCJE+4de39QybYKU3eZKhLp1qB1sxWDdk4vyE9wwZ0FfD4sTgK8cmqJkl2FXSqGkoQwRivDIqzQT5JF0MLSQ2r3dhktEAmpZUp6VbAU7mirAllBgvo1rzjt4TFiqM13pT29dSKdaFaX5HWhEW6Te2BoctVqfw9H95Ryqb0FW0eUXazp3lQbhQXr6PHq6L06TCuMTe6XCUwf6LP7Y6067WrQEweuxx1sGgMSwxOIrgvt6MZ7KyzqWwe8grpZg2p2kF8ioWP010t8OARQJDgNpKwOriT1FYFYxhhjui8TUGeThNeGUIBvpl5CFNHhklLnoeHbgYyYaavlWht6jHSGykIAy+1oqW5N3FPOhOJIrcG7P5pgvYuzXvev6fm3Q09wjtra/42cxJjInmL5wx/BDkKX5x14M5SscvwYlC1iX7WwdhtJrbRPQQQBlJylakqrdowOMN9MslsE7Wu97nYycYIjEWirqj8RhUAOUMXGDCMcSV/P6qvrgUslezH+RdA/dbY6lIcdBnJxhDhJ0bURl2Mc1KkYGgk8A46XzDJo/7MA4sLJm8GiliuNoFxPlSuk5oxy7ZXiwasY0riUCKgF8XXFxRTVUFVpi3VXFhjNpzCetmimfeuYHBH//t4sLw+2wLufaU8Liy1ynqb0BL8MhLh04NhiDkUtrtw0j1L/QDZcOhJncz/wj9Gw0wXM6rLLjlp65CprlEJQLfT5/JEteaKkmAb2AaVQFexIBugT24bIpFRtFnWMSpY5oCLaVDxpCKmtvJVJPTF+EZCmKu1L+4B1E+Tl6JJvfy/hPD202Vi6bRIG2QwPhmbMAuVWdGORSXoVn4Z0PahKq1g8bseMLg+1NfBGuR5ZNNnzUgyTYTaUSlsWSi675vhtluO6DojF+UHO3xOjsIO25b0a/nbwoTewflDaBEZF8MlNmufvDQ1HIYW4tZYM1N9lyYOmFJrL/unp6GHzR1v00uQ8ufvd3hbqDL7jdigZi8rXhjfVjTa7P1V38oKADnIeEWatSzpxVsceOS0M9i9SKjUYjV9zecDnm5DMSyq/+F0TwSCifUpWyoApzCSTaire/a9zA1s28ftZE4mFFA/mTe8Lt6LAnHLlQOIxlz49kpKDmg5aTQSSruAwyOVW6vY9w0RQAWLiJ287NC6FU3jz1ORmap8c+7TlXrfC3xdXs9cv5WoCwU/Ggvlpk5wyJ69mVElDyA1C61FqRle0weJtqJV3B3uu1aBA56uootHpTZMG9YypDOwVDT0rz5recNv0smdbrCoNXtPcHm8qZLERz8ugJrpYtKAhWWCixJpG6iwyaVmTute+pwICAa/6OQh3NZLNz5oza0S24TUwQbmwHuA/JRFLt6m5G6Cm9Gwh716Gok74qLUmuAoZPZh0oGlYPklNLMjf6+iPzqQ6CHUbrt4Biaa6h+j2JsTRw5gOLnvIsUhkMfnCHwrAycfA16TIsDk0DKcyR0lzWE5bv1w2n3BBwlBOd87o/EUQPV9i5URCHlEJTrOWOF3j/NsWaoglmQ3hk7bHsMOCOKoGrx5JlsJDRUPH7TPRo7rYH1XkXXxUEITFzF+n0lFi6wUBIpKOT6DlgoVppkmpaOM0SeCzplVzMqjjNz/JYdW45M3/UeAnekTQ5lYjT5FDXoo9XTI4CAjQJL8bxR2u+0zxt0zEmq7FVE78+NxE+LqPo1gsooOED8VC6ovL4VwrE/0erCMszA/4uVJ0ufM9CXbdaTNiP1sTqkCjkd6UwVoFcY8wHdaPU9bh64A/C/iaiLwKn/kfxjd5+QkJiM2X6ridt5Ggb+E0stjHU942bOpisKp0YRHVXbu8mI/tJr0G8NpIEh/F23/Jj/oz/0L62PkRyVzEKJgzuGqfISrd9EzGMnQmGV9GcN1rpqsATfVlKX0iw1TcHAzvGbXHmd0FCX5WYXcWKHdlOedrVrX9xp/ZCVd2NlxfSNza4zPFUTnr0y0uKdnPtrDwSxtcmCzGCptG9icEGJ+Am2XN0p9Xz1YmeOYS/mXmkLI3LEa3JORAjunIl3mc7xb5+3IBosQ6iQ5fDAKHbeTQhLfis9Dsja30eIhnjfBoJnxty+5ph/neJ/DLqEEFqEyVKpRsK57p5SV8j6cj3aKlgZZH6KM3+7la9vIeGTHD/wcgBgpJPnrWdBbFUsV6oEAHhArZW82KEosM6mvEZ1d9Q5IhzjhMt5N/Je0b8g8HqGdmSWfTKpqXyJ4Pa533J+zZN3pbULouc8AvVp3OnRvrhBwFDG49mYs6vGObtAJpih839fbygzHuJWLFrBlc1dcVxBo8BQWU7V9uVHPSZiPg3bPmLfl7DHIbQNUQEukfNJbH8qr0U88yFszYnSx/1/VK2sA8eut2DVNOiFbZ3BZBTRUrhcTXnePMAL8YHZJdtZkQQy2ODrvmS7VDcODVJBnhKRx82W1d621OnBkVYJNV3obJ0NNzxmYG8oPRtpFZGzqLFPlX1BwfghUiWm87yIRYWx9rpiKO0RHFyx6bJh3RWAuA/ao9r3Lab/P8BD0c0XnvSiCvKxaxxU3qF3UhFDTJe9KJVluDZWpUFyrJ3AMKcX3ZQOddoo14ttVcVUylJpqiRibKkkN1SYdZ7F5CNg/tlIyxix9gAKiDjtJ3sG4PwtYmXxt2QMFgxWRqFUbKQzVEapgsMSKG8VsZya4+SSlO1gHdSQXPc6kLexRG1QU0vVSBwN+ERrD3NrRb89iAlCjH1tf3e47waezCjuNL+N9DBO6TMA1/Iuj6icX8dtUjE16UodMCdtQ4wKEecJ62QsARy2bXoCo4fVHD1oTN88DdpaUasJ49/IonQIex5MmLPSHaLEGpRkfQ2L2GO/N9tH90KPxeKtpKvDGNh5s3OzZsZMoEkhpjq9PWnMibd5zQ5xYZcWZ67ee7ZDx24hZalzipn5eyUmNhjSlfRF1M5M7+eCYl7pfrYygiyFwOvbooQ4bK/toKA9HJHOhWD+PczVd4laxknM4ST+udal+toEXxJR2UZVX6AQxNcVxWU2qUf6J5ZVwiVinoIFqJniKW5aXdQnnhPP8pcT3+wAPVrrEwqEkyOoDpaD8WjKV/UOzQ87ICvFfARVXghixazhffTqMjxQdsVWYcRx/eBzVhIILySmTwUZkC1SyN8H1Cq5lDHQp12rMOH55iAwQKYpqB32L27nDKv/k3opUI45RJz72hgnTRxPnsF6TokQkmTR3sfakIcSMvdTR0iOFQHSfwrcthIDKvBko33A8v81Kw1O7uRajznDRptKIsR+eO0QewoDE+OTRVr9IDwVlgyVIQFtZNIJifwnkYnhEAuCT6Xe/OKivwuuayJkBQmNFtCkewYQLQ4fm/4oMhcUMn8BTLHbXgyNCo0XBwn22AEupYdiwa9C9iGYxbPLs3J4G+IbZYp05tRDkdKLQmysHrkMmBkYxd/FoVk/9SbiEEUxSOZFQLxRI4mBU1gJj7uaaDvIjFIPIIgOHg2Kitw2ObqnzX13nm5hfZodw57hIivbgc1Dk597bd/7+63iR1+VStkzoQLORgXAhE2v94KZUP1ajDbIch1h6mpe+EgxHsqWj3ypBzK2rEPFvD3z4kFh5sGclelpapmYoa0YZ9/+bdaJrqe0sqaWICVwh3N5rNVUuAPNY4rRnrFNWpb0F2XyA4ppeMJy1k4xK/OZ8zISFtJKiEkJ5Bo431CGn4vyjdBn3z4goqedUMSG+LeHRU3SpUPfkrh7K6H7qf9u+LKpAsSwGOLvqQb4TVjUGRjH8BthBR0KH/NJrR1XiRvgDiPK62AYsjIxvgmt9CrIZO3F2iOy6nlgPX1Gig/sqTa97I6ZpN82qOhSZyLeyVGCWi80KRPoCAkWOTe8SzhhiLdgTuFcC0KUCOfXw19yiO7w9kR0iorNYbh4k2uLVx9XzMIv3sz6suurAsmy8wmAQRvTpTXTbet9tVHDItg+ZCxkYnVgt8gxcBg5+UH5ELPqtIze4HbONBM4dzUw2J1/BD6lIKPpTItI1Zk0Z036hR6LeS5efX5C4vd6UMQC21E/4WUOvyddt0uA20Um8wxZZh8Y2wlbsMFrgxcV8Vvr3h2tImxO+JyW6xdwRNZn8rDm3K1z+ErTfB44/ZnvhZzhSlT5BdSjINhjHF/34xTlkwyQfUE0PAqQ7QqaDLaEXEZKSybPGgAb8vE6IRsmf2HyHxdF5KUSFq9MnDhsWIbvshTwgUTTsKqF5abtEPckVkT5p9XdeJrBJUiDwVAiuEw6ZcvpnXSuwrp3feRKFBQq7oeoyQncUdemvzuhcsAls05Ps17QTPWlq6SJRp41lXa+45Tjbl5spKZoG/qd63Ul1SdUF4pDoEraoppisKZbUIeH6StEj+wXrjYLZqEBhsPVHu/sMFQcEhxYZi58bC5e6Wr8U5DlIycRA5KFWNYDwBV9BqFr6KA7D03PfNGYn57xI8Zii9KQGTcdEaZ3m1rM5q8610Jy3JaLXjsFsVSPC6Tu49696KcVcxb88VBZlLmmw3HeFBr0Y12wuFa/HImcG99QwU5HBWFRUu9sGbHJKWaFf87ZVC1zqtmzhLr3b29sdAGLXIPmy3UXSILpD5Fv7T39T2s66K7XYh2dnXEGjVC8Dge7PmlgfVGBZ81vAr5CycMZ/zYpXGaIMGYf9vHnkFFArTPDVJybbIuRmqSyM3QPGwboTs2/MuTDJ++YbLeao7NZQdVy98tR6AQwa7M/x1WIYY3peXZY1KXELLXwDBFtHtEGqUGND3Fudnd2v+l1ePhxJ3/Yiae2n4brIc5E25Eqhbat7LIHkB7VgmFDMT+rpAA4DWJUigrLsXNhnrAoATMgzUaLg1E/LL3/rs3qLRqFaERZ6IgfxogLFkpHlKV0kl2d115eTv6RCzjkpTeigRbYwV9vhtj6EcgRartrtP97AxvH/HEyJz2lCONXEdwAwhqx5szA6L23rx+0iwTe8dibB0Vt+cQpBU+xFlklddUf/VHL9pm1IXYd+9few3Enqhi41Blnw8oHc7BW99RmItmIxswiSPJR8eIPfjaTbd35PtMICM5vdByz0Yt7w/bduBAphxd1ajn4Ts1fgQTkmb+rh1mPjp8eFuA4nHIvNJHFe3O6p7tj6Fi8ddc5DU7Kn/VU/ELz4nmywFee8za+AcKWULSwXHbmMVqS4Wu2Vcy62V9XG6ivUMzZ10txvKUX7ZUmu0XYFMWrXizwPMXTgEJXIwu9Z7hhT1qeenKlnosH1QluxRP+GM0cot53eSCjtNbyALaNPaABFeC9z1RGfVbjfzle79WwujCGJ4lHX/gN4Io22wgGq1np26noxYMYrlcxaJ+WGjqFIpM/MppfjomVmQbuiIQVI5u3Itm3qBCqfUCzpyZ0tTTEhQbTBruODkqM0Hmh16pKQ9kpZeMuesP76bsWdUA2KSZAot7UVM52IhqltdMv8V9edg1GJr/HgxOrOWm39M/pBEqKF0zq7JYV7zc+7V67n0R8qSBIrpUK8/Y2CC1h7B8+92YGhT8LetoqMkdkq5UNGZVlX6M3RnNz+NxqN3hwOD+9xHwSHK2DFd/+EYkrmIDbyIoQskTeZVC5gzlqNKiXO4Ux+ErXGMAeRrywong1aVTaWMwTC59WU3qAOgCSPVGH9v3pEf0zCmfGKDUIVqWQQeGswMi++4YUAFuwQ2rIo+2SZwHa/sdHK8YPZGckS+FeOHClDbAWS6AIeYOhhG9GWQEy8DAiYgiv3fQrPUr6acVAbhGFlD2h2HfPFvka/88pTHwKRUxWbNp79tmCyckIcFbURjkm2fN3CQ+Wx0XEbE+5DyPFUFGzLy7AwvFLkGOw2aUER546O7VNCIvHeNGqD36V4CL+rlNmQG4UvzQEQhB9OEJyOUucgg4ndz8RzKZOJMm+OL3U8O8/IY5o7V4uSMu/LUmBV/oCYrcv7P60tCZc2s4982DWoRCtWiozFcX7i4kljroYQ61mGfAIvI5q9/YwXfJQjPGNvkE56PuOb5GBnHaKin+C2FDiIFtOUg/Al1DaNAxIF4XB64QSD2dUCIYQaacel+HdMN4skVxl6okY+hYhNN14xyD/A3qhog5JI9AD4/ERQw6/lyUAuyOMdanPMzhpjxA5uaVW/LjabVjP4qeTPAQO7XxdcarP6PlsgoCfDU2ahXbJS/QcP1DDj+bXiDl4Fr/GviXo8DV5Dzpgjq7m9PgVwRVcyr5BulSUIjcfiP9EcLkMIgLQvEalDex6/lHYARJ+5diNg1D/VUWDuzjtuKJSDhsyFXYSPK9hTC6RIgpXYHDg6Idh4aphtjMzso4vDnL9HsZfgwrWCf2j/sRmzGI5Jl/nkTMSGPJYNSdjNTXy/1C2dp2YP/D/fLsgmQbCLHbPasR2iyQU5K3wGQOC7tpJp2Vngwz+GBQEyulu5Y9ZJW4CPg8X8uPPicuKKzRZ7rHMOM4zm6ubysY1FGHi4b4Idzy463ITzJH78OAQGA5Q1QxAGCrRDV4IOyMvMXLx3cw3wLC4pHig1+eiWn4rr8J+8/ELVduebp46xhprRhe3Mkr4qjXYNzC/JzaGiMmhaSA/8WwqENJap7g34CIvwdGzidMKg482Y6iWe6aAdd77FwrFsSP8TyPfc1lLfxq8miXuYCQzcZhwRvwAn66LKfE/ggwXi4eFP7gSDsYguXJ+l+BA978wlSe6yh62/KTbP9eOih62mdwhyNtKBHynFSUYVZoZeWcMuHpuPsL6Cb/Vi9pHA3bg4ZpK5teHhl2IPBL207wWWDw8ty05TZfukJ1mfDVUFl8wXiTkenJ1hJcwS7Pd4VmZ2sh3KepaB3HAvSBErcgnYW0Pgo7nXFETP393LkCLsXkqWyZ7F7ekShT7MjZzxsmYgYnZGNxPqGeTHAU0OeG9ee4Xe95fqyNbVsXFDVOGO01EGW4Ix0gRvQ6LFCLcMKTnwqohjofwnM7MrCrNalGTyJuKrdjTEMPbamWExvexhddM0TOzPKwijePxg8UVERPERe34Dr260nvqnMZUpcac7gh6rvrl0UMljeMHsWF1buHFrdRSW67NNMAzkgMbbnbGnVXEJGwSw3JsVVkBRWzpfp+czk0BQHuQw14Rdk36uqhObiAy1W/ymUzCEurQh9Q4lQpxGypD7CaN8+LHRf3TU4fJhRO+L/StdEn428ptGzMRSZvZXjTDRgW1HEpnnHKTIlKVd7tKdD7PbcIF0bLDp0QQdErsdL+A9ki9OBXdq9YEZDJx8KotLtk0JW0+GF1rV0tMyTgh3k5HVwGzS+uQVrdQJQlGz8PF4mfAHmg/j37e4fYwaZWxX8nkaI0Ksa42HKYHcENnZU7nVwlSwuxTlrVaHdvo7Z8KfGnJEIGFTpI/S/yQ4nBEzBNfT1ep1gATSngOt/8IF0BJcOBRtddeN3dBy7HOQBgG0CGAgmiEMwhc2Oy7Rkf3E3hBotyClKYUpazoMUzEuihhXnbtu1dQGCBDZ1ic10K2eP7Ui/8UrLz34M8XKHtWszgWO1KSHJDMlJNUWV8YUSDGVN5wpQFQly5VHpZxLThZZNiDtUxd7A1YF+3s+lAjduGAE="}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":5136,"cache_read_input_tokens":50627,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":5136},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYemvcS32dr8hzBLqwNp6","type":"assistant","uuid":"8910da7f-8a50-4f84-93fc-15b344d00765","timestamp":"2026-03-02T19:06:21.426Z"} +{"parentUuid":"8910da7f-8a50-4f84-93fc-15b344d00765","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_013NnRTfVdP7WHgS5CNPQxdE","type":"message","role":"assistant","content":[{"type":"text","text":"Let me check the env var constants used by the activity store and the full `initDatabase` function."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":5136,"cache_read_input_tokens":50627,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":5136},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYemvcS32dr8hzBLqwNp6","type":"assistant","uuid":"c011a946-d00e-4478-b7f6-313ad66ed992","timestamp":"2026-03-02T19:06:21.650Z"} +{"parentUuid":"c011a946-d00e-4478-b7f6-313ad66ed992","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_013NnRTfVdP7WHgS5CNPQxdE","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01DtwsDePUrABpviGfQNuJwf","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go","limit":50},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":5136,"cache_read_input_tokens":50627,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":5136},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYemvcS32dr8hzBLqwNp6","type":"assistant","uuid":"8f1cfb49-98a4-41a6-afea-118f9f51d3f8","timestamp":"2026-03-02T19:06:21.831Z"} +{"parentUuid":"8f1cfb49-98a4-41a6-afea-118f9f51d3f8","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01DtwsDePUrABpviGfQNuJwf","toolUseID":"toolu_01DtwsDePUrABpviGfQNuJwf","timestamp":"2026-03-02T19:06:21.864Z","uuid":"5458614d-a216-450a-bd1d-3f2965c7c478"} +{"parentUuid":"8f1cfb49-98a4-41a6-afea-118f9f51d3f8","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01DtwsDePUrABpviGfQNuJwf","type":"tool_result","content":" 1→package store\n 2→\n 3→import (\n 4→\t\"context\"\n 5→\t\"fmt\"\n 6→\t\"os\"\n 7→\t\"path/filepath\"\n 8→\t\"runtime\"\n 9→\t\"strconv\"\n 10→\t\"time\"\n 11→\n 12→\tlog \"github.com/sirupsen/logrus\"\n 13→\t\"gorm.io/driver/postgres\"\n 14→\t\"gorm.io/driver/sqlite\"\n 15→\t\"gorm.io/gorm\"\n 16→\t\"gorm.io/gorm/clause\"\n 17→\t\"gorm.io/gorm/logger\"\n 18→\n 19→\t\"github.com/netbirdio/netbird/management/server/activity\"\n 20→\t\"github.com/netbirdio/netbird/management/server/types\"\n 21→\t\"github.com/netbirdio/netbird/util/crypt\"\n 22→)\n 23→\n 24→const (\n 25→\t// eventSinkDB is the default name of the events database\n 26→\teventSinkDB = \"events.db\"\n 27→\n 28→\tfallbackName = \"unknown\"\n 29→\tfallbackEmail = \"unknown@unknown.com\"\n 30→\n 31→\tgcmEncAlgo = \"GCM\"\n 32→\n 33→\tstoreEngineEnv = \"NB_ACTIVITY_EVENT_STORE_ENGINE\"\n 34→\tpostgresDsnEnv = \"NB_ACTIVITY_EVENT_POSTGRES_DSN\"\n 35→\tsqlMaxOpenConnsEnv = \"NB_SQL_MAX_OPEN_CONNS\"\n 36→)\n 37→\n 38→type eventWithNames struct {\n 39→\tactivity.Event\n 40→\tInitiatorName string\n 41→\tInitiatorEmail string\n 42→\tTargetName string\n 43→\tTargetEmail string\n 44→}\n 45→\n 46→// Store is the implementation of the activity.Store interface backed by SQLite\n 47→type Store struct {\n 48→\tdb *gorm.DB\n 49→\tfieldEncrypt *crypt.FieldEncrypt\n 50→}"}]},"uuid":"41707209-a627-4a09-929a-5f06bcb4bcf0","timestamp":"2026-03-02T19:06:21.863Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go","content":"package store\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/driver/sqlite\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n\t\"gorm.io/gorm/logger\"\n\n\t\"github.com/netbirdio/netbird/management/server/activity\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n)\n\nconst (\n\t// eventSinkDB is the default name of the events database\n\teventSinkDB = \"events.db\"\n\n\tfallbackName = \"unknown\"\n\tfallbackEmail = \"unknown@unknown.com\"\n\n\tgcmEncAlgo = \"GCM\"\n\n\tstoreEngineEnv = \"NB_ACTIVITY_EVENT_STORE_ENGINE\"\n\tpostgresDsnEnv = \"NB_ACTIVITY_EVENT_POSTGRES_DSN\"\n\tsqlMaxOpenConnsEnv = \"NB_SQL_MAX_OPEN_CONNS\"\n)\n\ntype eventWithNames struct {\n\tactivity.Event\n\tInitiatorName string\n\tInitiatorEmail string\n\tTargetName string\n\tTargetEmail string\n}\n\n// Store is the implementation of the activity.Store interface backed by SQLite\ntype Store struct {\n\tdb *gorm.DB\n\tfieldEncrypt *crypt.FieldEncrypt\n}","numLines":50,"startLine":1,"totalLines":296}},"sourceToolAssistantUUID":"8f1cfb49-98a4-41a6-afea-118f9f51d3f8"} +{"parentUuid":"41707209-a627-4a09-929a-5f06bcb4bcf0","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_013NnRTfVdP7WHgS5CNPQxdE","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01LnqAZsQrVxhLYREdUnLxMQ","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go","offset":235,"limit":40},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":5136,"cache_read_input_tokens":50627,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":5136},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYemvcS32dr8hzBLqwNp6","type":"assistant","uuid":"b52d2fb5-3e92-4ba0-9c07-0a1ef1ac772f","timestamp":"2026-03-02T19:06:22.168Z"} +{"parentUuid":"b52d2fb5-3e92-4ba0-9c07-0a1ef1ac772f","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01LnqAZsQrVxhLYREdUnLxMQ","toolUseID":"toolu_01LnqAZsQrVxhLYREdUnLxMQ","timestamp":"2026-03-02T19:06:22.184Z","uuid":"27c41f33-30e0-4434-a728-7e5689976bf7"} +{"parentUuid":"b52d2fb5-3e92-4ba0-9c07-0a1ef1ac772f","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01LnqAZsQrVxhLYREdUnLxMQ","type":"tool_result","content":" 235→\t\t\treturn err\n 236→\t\t}\n 237→\t\treturn sql.Close()\n 238→\t}\n 239→\treturn nil\n 240→}\n 241→\n 242→func initDatabase(ctx context.Context, dataDir string) (*gorm.DB, error) {\n 243→\tvar dialector gorm.Dialector\n 244→\tvar storeEngine = types.SqliteStoreEngine\n 245→\n 246→\tif engine, ok := os.LookupEnv(storeEngineEnv); ok {\n 247→\t\tstoreEngine = types.Engine(engine)\n 248→\t}\n 249→\n 250→\tswitch storeEngine {\n 251→\tcase types.SqliteStoreEngine:\n 252→\t\tdialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB))\n 253→\tcase types.PostgresStoreEngine:\n 254→\t\tdsn, ok := os.LookupEnv(postgresDsnEnv)\n 255→\t\tif !ok {\n 256→\t\t\treturn nil, fmt.Errorf(\"%s environment variable not set\", postgresDsnEnv)\n 257→\t\t}\n 258→\t\tdialector = postgres.Open(dsn)\n 259→\tdefault:\n 260→\t\treturn nil, fmt.Errorf(\"unsupported store engine: %s\", storeEngine)\n 261→\t}\n 262→\tlog.WithContext(ctx).Infof(\"using %s as activity event store engine\", storeEngine)\n 263→\n 264→\tdb, err := gorm.Open(dialector, &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})\n 265→\tif err != nil {\n 266→\t\treturn nil, fmt.Errorf(\"open db connection: %w\", err)\n 267→\t}\n 268→\n 269→\treturn configureConnectionPool(db, storeEngine)\n 270→}\n 271→\n 272→func configureConnectionPool(db *gorm.DB, storeEngine types.Engine) (*gorm.DB, error) {\n 273→\tsqlDB, err := db.DB()\n 274→\tif err != nil {"}]},"uuid":"a404f4ff-3733-494a-9dde-d5872346989c","timestamp":"2026-03-02T19:06:22.183Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go","content":"\t\t\treturn err\n\t\t}\n\t\treturn sql.Close()\n\t}\n\treturn nil\n}\n\nfunc initDatabase(ctx context.Context, dataDir string) (*gorm.DB, error) {\n\tvar dialector gorm.Dialector\n\tvar storeEngine = types.SqliteStoreEngine\n\n\tif engine, ok := os.LookupEnv(storeEngineEnv); ok {\n\t\tstoreEngine = types.Engine(engine)\n\t}\n\n\tswitch storeEngine {\n\tcase types.SqliteStoreEngine:\n\t\tdialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB))\n\tcase types.PostgresStoreEngine:\n\t\tdsn, ok := os.LookupEnv(postgresDsnEnv)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"%s environment variable not set\", postgresDsnEnv)\n\t\t}\n\t\tdialector = postgres.Open(dsn)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported store engine: %s\", storeEngine)\n\t}\n\tlog.WithContext(ctx).Infof(\"using %s as activity event store engine\", storeEngine)\n\n\tdb, err := gorm.Open(dialector, &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open db connection: %w\", err)\n\t}\n\n\treturn configureConnectionPool(db, storeEngine)\n}\n\nfunc configureConnectionPool(db *gorm.DB, storeEngine types.Engine) (*gorm.DB, error) {\n\tsqlDB, err := db.DB()\n\tif err != nil {","numLines":40,"startLine":235,"totalLines":296}},"sourceToolAssistantUUID":"b52d2fb5-3e92-4ba0-9c07-0a1ef1ac772f"} +{"parentUuid":"a404f4ff-3733-494a-9dde-d5872346989c","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_013NnRTfVdP7WHgS5CNPQxdE","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01LyuHJgxKrCg4Ckiv7mfycD","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go","offset":2725,"limit":25},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":5136,"cache_read_input_tokens":50627,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":5136},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYemvcS32dr8hzBLqwNp6","type":"assistant","uuid":"8bf48967-582f-41da-b8da-70dc64899e64","timestamp":"2026-03-02T19:06:22.691Z"} +{"parentUuid":"8bf48967-582f-41da-b8da-70dc64899e64","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01LyuHJgxKrCg4Ckiv7mfycD","toolUseID":"toolu_01LyuHJgxKrCg4Ckiv7mfycD","timestamp":"2026-03-02T19:06:22.719Z","uuid":"6ea48fa6-31ae-4a3e-9ec7-ea33ae11c315"} +{"parentUuid":"8bf48967-582f-41da-b8da-70dc64899e64","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01LyuHJgxKrCg4Ckiv7mfycD","type":"tool_result","content":" 2725→func (s *SqlStore) GetStoreEngine() types.Engine {\n 2726→\treturn s.storeEngine\n 2727→}\n 2728→\n 2729→// NewSqliteStore creates a new SQLite store.\n 2730→func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n 2731→\tstoreStr := fmt.Sprintf(\"%s?cache=shared\", storeSqliteFileName)\n 2732→\tif runtime.GOOS == \"windows\" {\n 2733→\t\t// Vo avoid `The process cannot access the file because it is being used by another process` on Windows\n 2734→\t\tstoreStr = storeSqliteFileName\n 2735→\t}\n 2736→\n 2737→\tfile := filepath.Join(dataDir, storeStr)\n 2738→\tdb, err := gorm.Open(sqlite.Open(file), getGormConfig())\n 2739→\tif err != nil {\n 2740→\t\treturn nil, err\n 2741→\t}\n 2742→\n 2743→\treturn NewSqlStore(ctx, db, types.SqliteStoreEngine, metrics, skipMigration)\n 2744→}\n 2745→\n 2746→// NewPostgresqlStore creates a new Postgres store.\n 2747→func NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n 2748→\tdb, err := gorm.Open(postgres.Open(dsn), getGormConfig())\n 2749→\tif err != nil {"}]},"uuid":"29fb6716-bc40-4093-934a-ca07540a4aa4","timestamp":"2026-03-02T19:06:22.718Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go","content":"func (s *SqlStore) GetStoreEngine() types.Engine {\n\treturn s.storeEngine\n}\n\n// NewSqliteStore creates a new SQLite store.\nfunc NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tstoreStr := fmt.Sprintf(\"%s?cache=shared\", storeSqliteFileName)\n\tif runtime.GOOS == \"windows\" {\n\t\t// Vo avoid `The process cannot access the file because it is being used by another process` on Windows\n\t\tstoreStr = storeSqliteFileName\n\t}\n\n\tfile := filepath.Join(dataDir, storeStr)\n\tdb, err := gorm.Open(sqlite.Open(file), getGormConfig())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn NewSqlStore(ctx, db, types.SqliteStoreEngine, metrics, skipMigration)\n}\n\n// NewPostgresqlStore creates a new Postgres store.\nfunc NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tdb, err := gorm.Open(postgres.Open(dsn), getGormConfig())\n\tif err != nil {","numLines":25,"startLine":2725,"totalLines":5278}},"sourceToolAssistantUUID":"8bf48967-582f-41da-b8da-70dc64899e64"} +{"parentUuid":"29fb6716-bc40-4093-934a-ca07540a4aa4","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_013NnRTfVdP7WHgS5CNPQxdE","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01R2jTvNRa5yHjtjyPAKUkY4","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/config/config.go","offset":36,"limit":40},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":5136,"cache_read_input_tokens":50627,"output_tokens":2670,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":5136,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYemvcS32dr8hzBLqwNp6","type":"assistant","uuid":"e8d5a6e4-f481-42df-8965-279b3cf9bfb7","timestamp":"2026-03-02T19:06:23.561Z"} +{"parentUuid":"e8d5a6e4-f481-42df-8965-279b3cf9bfb7","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01R2jTvNRa5yHjtjyPAKUkY4","toolUseID":"toolu_01R2jTvNRa5yHjtjyPAKUkY4","timestamp":"2026-03-02T19:06:23.576Z","uuid":"e2de070b-e368-45bd-b853-a509cd46ccc9"} +{"parentUuid":"e8d5a6e4-f481-42df-8965-279b3cf9bfb7","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01R2jTvNRa5yHjtjyPAKUkY4","type":"tool_result","content":" 36→// Config of the Management service\n 37→type Config struct {\n 38→\tStuns []*Host\n 39→\tTURNConfig *TURNConfig\n 40→\tRelay *Relay\n 41→\tSignal *Host\n 42→\n 43→\tDatadir string\n 44→\tDataStoreEncryptionKey string\n 45→\n 46→\tHttpConfig *HttpServerConfig\n 47→\n 48→\tIdpManagerConfig *idp.Config\n 49→\n 50→\tDeviceAuthorizationFlow *DeviceAuthorizationFlow\n 51→\n 52→\tPKCEAuthorizationFlow *PKCEAuthorizationFlow\n 53→\n 54→\tStoreConfig StoreConfig\n 55→\n 56→\tReverseProxy ReverseProxy\n 57→\n 58→\t// disable default all-to-all policy\n 59→\tDisableDefaultPolicy bool\n 60→\n 61→\t// EmbeddedIdP contains configuration for the embedded Dex OIDC provider.\n 62→\t// When set, Dex will be embedded in the management server and serve requests at /oauth2/\n 63→\tEmbeddedIdP *idp.EmbeddedIdPConfig\n 64→}\n 65→\n 66→// GetAuthAudiences returns the audience from the http config and device authorization flow config\n 67→func (c Config) GetAuthAudiences() []string {\n 68→\taudiences := []string{c.HttpConfig.AuthAudience}\n 69→\n 70→\tif c.HttpConfig.ExtraAuthAudience != \"\" {\n 71→\t\taudiences = append(audiences, c.HttpConfig.ExtraAuthAudience)\n 72→\t}\n 73→\n 74→\tif c.DeviceAuthorizationFlow != nil && c.DeviceAuthorizationFlow.ProviderConfig.Audience != \"\" {\n 75→\t\taudiences = append(audiences, c.DeviceAuthorizationFlow.ProviderConfig.Audience)"}]},"uuid":"09d90af1-65fc-4cf0-99b3-f02fed74a964","timestamp":"2026-03-02T19:06:23.576Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/config/config.go","content":"// Config of the Management service\ntype Config struct {\n\tStuns []*Host\n\tTURNConfig *TURNConfig\n\tRelay *Relay\n\tSignal *Host\n\n\tDatadir string\n\tDataStoreEncryptionKey string\n\n\tHttpConfig *HttpServerConfig\n\n\tIdpManagerConfig *idp.Config\n\n\tDeviceAuthorizationFlow *DeviceAuthorizationFlow\n\n\tPKCEAuthorizationFlow *PKCEAuthorizationFlow\n\n\tStoreConfig StoreConfig\n\n\tReverseProxy ReverseProxy\n\n\t// disable default all-to-all policy\n\tDisableDefaultPolicy bool\n\n\t// EmbeddedIdP contains configuration for the embedded Dex OIDC provider.\n\t// When set, Dex will be embedded in the management server and serve requests at /oauth2/\n\tEmbeddedIdP *idp.EmbeddedIdPConfig\n}\n\n// GetAuthAudiences returns the audience from the http config and device authorization flow config\nfunc (c Config) GetAuthAudiences() []string {\n\taudiences := []string{c.HttpConfig.AuthAudience}\n\n\tif c.HttpConfig.ExtraAuthAudience != \"\" {\n\t\taudiences = append(audiences, c.HttpConfig.ExtraAuthAudience)\n\t}\n\n\tif c.DeviceAuthorizationFlow != nil && c.DeviceAuthorizationFlow.ProviderConfig.Audience != \"\" {\n\t\taudiences = append(audiences, c.DeviceAuthorizationFlow.ProviderConfig.Audience)","numLines":40,"startLine":36,"totalLines":213}},"sourceToolAssistantUUID":"e8d5a6e4-f481-42df-8965-279b3cf9bfb7"} +{"parentUuid":"09d90af1-65fc-4cf0-99b3-f02fed74a964","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01HirQ9yy3mL5LKuKwrhqjuw","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"Now I have a clear picture. Let me plan the changes:\n\n**What needs to happen:**\n\n1. **Combined `StoreConfig`** - Add `File` field\n2. **Auth store** (`idp.db`) - Already has `File` plumbing in `idp.EmbeddedStorageTypeConfig.File`. In `buildEmbeddedIdPConfig`, use `authStore.File` if set instead of `path.Join(mgmt.DataDir, \"idp.db\")`\n3. **Activity store** (`events.db`) - Uses env vars. I could add a new env var for the sqlite file path, OR modify the activity store to accept a file parameter. Since it already uses `NB_ACTIVITY_EVENT_STORE_ENGINE` env var pattern, adding `NB_ACTIVITY_EVENT_SQLITE_FILE` env var would be consistent.\n\n\n4. **Main store** (`store.db`) - `store.NewSqliteStore` takes only `dataDir`. I should follow the established pattern and use an env var like `NB_STORE_ENGINE_SQLITE_FILE` to specify the sqlite file path, which aligns with how the combined server already handles DSN configuration for other stores.","signature":"EvQKCkYICxgCKkAvIfOc/QfMkXLnCprydiqatuq/RaZ9f2hoiiyRj0KA5Mz/L0PmAzEDZ7ijgBIIjqy326bqujdq+YBjlBZ2ohgUEgwzn+Q/YrEa/36Q9YEaDN0sZcouhM6f3mmVnyIwUEf4vSNMF57JgK4gFmcszQ1I66J6+Lsaj3WXlbpoJUQYQMvnLSO0VaBf0z3OHKd0KtsJ77+nKX71EcG+1yPpGr3IhN+nDBOLOpR0wdZ/4sZ1TdjCjlTMcu/GoZRRDxLQOvUFLHK2JfEClZiVkPDk5zw7Pczx0r0mm2uCyS+V1VDDqev4NGjxl7wftRXmPwJAe8w19395TtO/z7XQ/AAP+rQxhacby3B6BA26nW+rA1xOJzukCJfLiDjkhUwi2SaR4oNlp/XNjsMJkE8s2p7bRdEy6hE8R95AUHAg7NU5+K5wB/jY/pxJ3qta37viQeJB5udBTveVcTixQzvVxls1+b3ZvvHHdNIRChRCtZPIFmwd28QsqFsDF9GbReK6pZk95IHyCUASZly/QpaZCl6vcwt2APqwvgsLSTUo1olK2k/eclxxoZ1u903Zmbm0nPtIF1FBvPElngwsS9/px4P6c9V3u3PiPiViGl3ObdCw2sF+2Y1Llmc1G9VR97aaZDMtKDMCQ0GfHfWG5K0ej5QeOVdRJVESkTs74qc9uOmL1Tuch4n32tyQC+4e/ab8TWyMxBkERXtAk6H5d6sYW1qT3TlkDVqUwaOzQ0S/0OdzZWHUkJQ6tqjsFo+kOBRCGgbxRrFnhoK9eXBo4IclZH2sp0wFPpeqXOI1KbFjjDn5yRzOJcGhWq7wZK5Ra1mPREhVgvTRGz+cH1Mhphgbm9CAhdRDRSXut1Hc3eLUZNNqbcxrzT1i9x9wTAx3wUUFY7yVA+GNHJtlsqYgn/yrUM79+pIsxt+qk0Q6Wq92at7dO8uqXmJROFKpFoIWQVqvQsonfmF+vOMWKaiOaBe4zrdh3HyqP1IRnouQE/2vM0Jf0IKPrx3X22CDU2a1qOuyCDIJ5vpsVU/gK6KlQzt/0qVlBKD14XBrbM/CZGDT+gqpRVsnAldiuLhm8HrZGJuRSBZFiZQQVXFJOs2Hu/TfnF4QhxNXoyvMA3m/IK7947mG0jekNsFx8OtrOfF5UnI0xnvOrWDSKXnZRTjM+lXFuD++wrBTS6iXHtQARjnIFOuBPUk6JTrHnJhtOPWvMY2fbVtZZi3dX2TO4TRkip1w5LkiXcuXl1siaFnkjSJiQ4Jj+cRQsmOxnlGxrv90TVwEFCYrPpmxxZ/3B9DubQ5cV1lD6ewH2DVCdAfVLrpLXnirrBbyPk8eHYoTQYJ2K3r9Jkag58CfgMiPtbV4klupyMWlqBTpUi1PTy5XL6cmTv1NgENHh+3c+rw6z+UcJvUrXxzQTiZlPPPB2o9SBvpGDxj/GLwwo87fd9AFWwqAi8EAGMtq+U596ONec+1hqUgoo42W9f9DpaiNJ+rXhlk5H4uw7szu+sKotXnywwMWZysVzqVWyZmjKeEfpHBEKt7Xli0u6GB1Hv8GeANDxw6rMAlI1GkzB6OZ4gku51DiBwMU/0GMuWExgYwb6vjYBEKahI9cRtxkR1a5yAtae4W38cXSyqMaw4OUM3uHKSOyPaHmOlv0dZNSRmrsII85YjWGarws725ocusYsS6ZCyt4zTv8Tr1VaiK1uG14oL6c3tg2U7ypmBgX4zn1mZmhfh3zcWluXsA2qmbKfYLIlhT7XkNy1MJ9//nLM05ZfsWRytKS1UXllexq1LYdvlmcCrHcbLDIxqETmaaEBpWjXWCSJ4MO1Lm/3XB3tIVkgpvyHpvIpkyH+oJPWwDbRrl0zWpk2BgB"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":4768,"cache_read_input_tokens":55763,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":4768},"output_tokens":10,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYen1UFJsYXR7B1jYiKqY","type":"assistant","uuid":"103b7306-6903-4d0a-91b7-1f8b737287e5","timestamp":"2026-03-02T19:06:35.970Z"} +{"parentUuid":"103b7306-6903-4d0a-91b7-1f8b737287e5","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01HirQ9yy3mL5LKuKwrhqjuw","type":"message","role":"assistant","content":[{"type":"text","text":"Let me check what env vars the main store uses and how the store boot process works."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":4768,"cache_read_input_tokens":55763,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":4768},"output_tokens":10,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYen1UFJsYXR7B1jYiKqY","type":"assistant","uuid":"349829ed-ed56-4c32-b0e0-6b6d4cd33fb1","timestamp":"2026-03-02T19:06:36.002Z"} +{"parentUuid":"349829ed-ed56-4c32-b0e0-6b6d4cd33fb1","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01HirQ9yy3mL5LKuKwrhqjuw","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01EXBenQEYYhBHxe91UCxHfE","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go","offset":290,"limit":80},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":4768,"cache_read_input_tokens":55763,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":4768},"output_tokens":10,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYen1UFJsYXR7B1jYiKqY","type":"assistant","uuid":"24c24ad6-d926-464b-aa2d-da4c7b18d1f2","timestamp":"2026-03-02T19:06:36.830Z"} +{"parentUuid":"24c24ad6-d926-464b-aa2d-da4c7b18d1f2","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01EXBenQEYYhBHxe91UCxHfE","toolUseID":"toolu_01EXBenQEYYhBHxe91UCxHfE","timestamp":"2026-03-02T19:06:36.864Z","uuid":"626c949d-002c-49f4-a4d8-ca69556a0b08"} +{"parentUuid":"24c24ad6-d926-464b-aa2d-da4c7b18d1f2","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01EXBenQEYYhBHxe91UCxHfE","type":"tool_result","content":" 290→// lookupDSNEnv checks the NB_ env var first, then falls back to the legacy NETBIRD_ env var.\n 291→func lookupDSNEnv(nbKey, legacyKey string) (string, bool) {\n 292→\tif v, ok := os.LookupEnv(nbKey); ok {\n 293→\t\treturn v, true\n 294→\t}\n 295→\treturn os.LookupEnv(legacyKey)\n 296→}\n 297→\n 298→var supportedEngines = []types.Engine{types.SqliteStoreEngine, types.PostgresStoreEngine, types.MysqlStoreEngine}\n 299→\n 300→func getStoreEngineFromEnv() types.Engine {\n 301→\t// NETBIRD_STORE_ENGINE supposed to be used in tests. Otherwise, rely on the config file.\n 302→\tkind, ok := os.LookupEnv(\"NETBIRD_STORE_ENGINE\")\n 303→\tif !ok {\n 304→\t\treturn \"\"\n 305→\t}\n 306→\n 307→\tvalue := types.Engine(strings.ToLower(kind))\n 308→\tif slices.Contains(supportedEngines, value) {\n 309→\t\treturn value\n 310→\t}\n 311→\n 312→\treturn types.SqliteStoreEngine\n 313→}\n 314→\n 315→// getStoreEngine determines the store engine to use.\n 316→// If no engine is specified, it attempts to retrieve it from the environment.\n 317→// If still not specified, it defaults to using SQLite.\n 318→// Additionally, it handles the migration from a JSON store file to SQLite if applicable.\n 319→func getStoreEngine(ctx context.Context, dataDir string, kind types.Engine) types.Engine {\n 320→\tif kind == \"\" {\n 321→\t\tkind = getStoreEngineFromEnv()\n 322→\t\tif kind == \"\" {\n 323→\t\t\tkind = types.SqliteStoreEngine\n 324→\n 325→\t\t\t// Migrate if it is the first run with a JSON file existing and no SQLite file present\n 326→\t\t\tjsonStoreFile := filepath.Join(dataDir, storeFileName)\n 327→\t\t\tsqliteStoreFile := filepath.Join(dataDir, storeSqliteFileName)\n 328→\n 329→\t\t\tif util.FileExists(jsonStoreFile) && !util.FileExists(sqliteStoreFile) {\n 330→\t\t\t\tlog.WithContext(ctx).Warnf(\"unsupported store engine specified, but found %s. Automatically migrating to SQLite.\", jsonStoreFile)\n 331→\n 332→\t\t\t\t// Attempt to migratePreAuto from JSON store to SQLite\n 333→\t\t\t\tif err := MigrateFileStoreToSqlite(ctx, dataDir); err != nil {\n 334→\t\t\t\t\tlog.WithContext(ctx).Errorf(\"failed to migratePreAuto filestore to SQLite: %v\", err)\n 335→\t\t\t\t\tkind = types.FileStoreEngine\n 336→\t\t\t\t}\n 337→\t\t\t}\n 338→\t\t}\n 339→\t}\n 340→\n 341→\treturn kind\n 342→}\n 343→\n 344→// NewStore creates a new store based on the provided engine type, data directory, and telemetry metrics\n 345→func NewStore(ctx context.Context, kind types.Engine, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {\n 346→\tkind = getStoreEngine(ctx, dataDir, kind)\n 347→\n 348→\tif err := checkFileStoreEngine(kind, dataDir); err != nil {\n 349→\t\treturn nil, err\n 350→\t}\n 351→\n 352→\tswitch kind {\n 353→\tcase types.SqliteStoreEngine:\n 354→\t\tlog.WithContext(ctx).Info(\"using SQLite store engine\")\n 355→\t\treturn NewSqliteStore(ctx, dataDir, metrics, skipMigration)\n 356→\tcase types.PostgresStoreEngine:\n 357→\t\tlog.WithContext(ctx).Info(\"using Postgres store engine\")\n 358→\t\treturn newPostgresStore(ctx, metrics, skipMigration)\n 359→\tcase types.MysqlStoreEngine:\n 360→\t\tlog.WithContext(ctx).Info(\"using MySQL store engine\")\n 361→\t\treturn newMysqlStore(ctx, metrics, skipMigration)\n 362→\tdefault:\n 363→\t\treturn nil, fmt.Errorf(\"unsupported kind of store: %s\", kind)\n 364→\t}\n 365→}\n 366→\n 367→func checkFileStoreEngine(kind types.Engine, dataDir string) error {\n 368→\tif kind == types.FileStoreEngine {\n 369→\t\tstoreFile := filepath.Join(dataDir, storeFileName)"}]},"uuid":"50d51ac1-fb8d-409f-b468-3db9633a9855","timestamp":"2026-03-02T19:06:36.863Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/store.go","content":"// lookupDSNEnv checks the NB_ env var first, then falls back to the legacy NETBIRD_ env var.\nfunc lookupDSNEnv(nbKey, legacyKey string) (string, bool) {\n\tif v, ok := os.LookupEnv(nbKey); ok {\n\t\treturn v, true\n\t}\n\treturn os.LookupEnv(legacyKey)\n}\n\nvar supportedEngines = []types.Engine{types.SqliteStoreEngine, types.PostgresStoreEngine, types.MysqlStoreEngine}\n\nfunc getStoreEngineFromEnv() types.Engine {\n\t// NETBIRD_STORE_ENGINE supposed to be used in tests. Otherwise, rely on the config file.\n\tkind, ok := os.LookupEnv(\"NETBIRD_STORE_ENGINE\")\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\tvalue := types.Engine(strings.ToLower(kind))\n\tif slices.Contains(supportedEngines, value) {\n\t\treturn value\n\t}\n\n\treturn types.SqliteStoreEngine\n}\n\n// getStoreEngine determines the store engine to use.\n// If no engine is specified, it attempts to retrieve it from the environment.\n// If still not specified, it defaults to using SQLite.\n// Additionally, it handles the migration from a JSON store file to SQLite if applicable.\nfunc getStoreEngine(ctx context.Context, dataDir string, kind types.Engine) types.Engine {\n\tif kind == \"\" {\n\t\tkind = getStoreEngineFromEnv()\n\t\tif kind == \"\" {\n\t\t\tkind = types.SqliteStoreEngine\n\n\t\t\t// Migrate if it is the first run with a JSON file existing and no SQLite file present\n\t\t\tjsonStoreFile := filepath.Join(dataDir, storeFileName)\n\t\t\tsqliteStoreFile := filepath.Join(dataDir, storeSqliteFileName)\n\n\t\t\tif util.FileExists(jsonStoreFile) && !util.FileExists(sqliteStoreFile) {\n\t\t\t\tlog.WithContext(ctx).Warnf(\"unsupported store engine specified, but found %s. Automatically migrating to SQLite.\", jsonStoreFile)\n\n\t\t\t\t// Attempt to migratePreAuto from JSON store to SQLite\n\t\t\t\tif err := MigrateFileStoreToSqlite(ctx, dataDir); err != nil {\n\t\t\t\t\tlog.WithContext(ctx).Errorf(\"failed to migratePreAuto filestore to SQLite: %v\", err)\n\t\t\t\t\tkind = types.FileStoreEngine\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn kind\n}\n\n// NewStore creates a new store based on the provided engine type, data directory, and telemetry metrics\nfunc NewStore(ctx context.Context, kind types.Engine, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {\n\tkind = getStoreEngine(ctx, dataDir, kind)\n\n\tif err := checkFileStoreEngine(kind, dataDir); err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch kind {\n\tcase types.SqliteStoreEngine:\n\t\tlog.WithContext(ctx).Info(\"using SQLite store engine\")\n\t\treturn NewSqliteStore(ctx, dataDir, metrics, skipMigration)\n\tcase types.PostgresStoreEngine:\n\t\tlog.WithContext(ctx).Info(\"using Postgres store engine\")\n\t\treturn newPostgresStore(ctx, metrics, skipMigration)\n\tcase types.MysqlStoreEngine:\n\t\tlog.WithContext(ctx).Info(\"using MySQL store engine\")\n\t\treturn newMysqlStore(ctx, metrics, skipMigration)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported kind of store: %s\", kind)\n\t}\n}\n\nfunc checkFileStoreEngine(kind types.Engine, dataDir string) error {\n\tif kind == types.FileStoreEngine {\n\t\tstoreFile := filepath.Join(dataDir, storeFileName)","numLines":80,"startLine":290,"totalLines":815}},"sourceToolAssistantUUID":"24c24ad6-d926-464b-aa2d-da4c7b18d1f2"} +{"parentUuid":"50d51ac1-fb8d-409f-b468-3db9633a9855","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01HirQ9yy3mL5LKuKwrhqjuw","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_013hU7mSNDzNZsMHZbuJ5won","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/boot.go","offset":50,"limit":50},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":4768,"cache_read_input_tokens":55763,"output_tokens":619,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":4768,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYen1UFJsYXR7B1jYiKqY","type":"assistant","uuid":"d03884d9-fa3f-45a6-bccc-e07ad822ef48","timestamp":"2026-03-02T19:06:37.662Z"} +{"parentUuid":"d03884d9-fa3f-45a6-bccc-e07ad822ef48","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_013hU7mSNDzNZsMHZbuJ5won","toolUseID":"toolu_013hU7mSNDzNZsMHZbuJ5won","timestamp":"2026-03-02T19:06:37.677Z","uuid":"e9c4134c-0abf-42b5-ac96-b723fb6d71d9"} +{"parentUuid":"d03884d9-fa3f-45a6-bccc-e07ad822ef48","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_013hU7mSNDzNZsMHZbuJ5won","type":"tool_result","content":" 50→func (s *BaseServer) Metrics() telemetry.AppMetrics {\n 51→\treturn Create(s, func() telemetry.AppMetrics {\n 52→\t\tappMetrics, err := telemetry.NewDefaultAppMetrics(context.Background())\n 53→\t\tif err != nil {\n 54→\t\t\tlog.Fatalf(\"error while creating app metrics: %s\", err)\n 55→\t\t}\n 56→\t\treturn appMetrics\n 57→\t})\n 58→}\n 59→\n 60→func (s *BaseServer) Store() store.Store {\n 61→\treturn Create(s, func() store.Store {\n 62→\t\tstore, err := store.NewStore(context.Background(), s.Config.StoreConfig.Engine, s.Config.Datadir, s.Metrics(), false)\n 63→\t\tif err != nil {\n 64→\t\t\tlog.Fatalf(\"failed to create store: %v\", err)\n 65→\t\t}\n 66→\n 67→\t\tif s.Config.DataStoreEncryptionKey != \"\" {\n 68→\t\t\tfieldEncrypt, err := crypt.NewFieldEncrypt(s.Config.DataStoreEncryptionKey)\n 69→\t\t\tif err != nil {\n 70→\t\t\t\tlog.Fatalf(\"failed to create field encryptor: %v\", err)\n 71→\t\t\t}\n 72→\t\t\tstore.SetFieldEncrypt(fieldEncrypt)\n 73→\t\t}\n 74→\n 75→\t\treturn store\n 76→\t})\n 77→}\n 78→\n 79→func (s *BaseServer) EventStore() activity.Store {\n 80→\treturn Create(s, func() activity.Store {\n 81→\t\tintegrationMetrics, err := integrations.InitIntegrationMetrics(context.Background(), s.Metrics())\n 82→\t\tif err != nil {\n 83→\t\t\tlog.Fatalf(\"failed to initialize integration metrics: %v\", err)\n 84→\t\t}\n 85→\n 86→\t\teventStore, _, err := integrations.InitEventStore(context.Background(), s.Config.Datadir, s.Config.DataStoreEncryptionKey, integrationMetrics)\n 87→\t\tif err != nil {\n 88→\t\t\tlog.Fatalf(\"failed to initialize event store: %v\", err)\n 89→\t\t}\n 90→\n 91→\t\treturn eventStore\n 92→\t})\n 93→}\n 94→\n 95→func (s *BaseServer) APIHandler() http.Handler {\n 96→\treturn Create(s, func() http.Handler {\n 97→\t\thttpAPIHandler, err := nbhttp.NewAPIHandler(context.Background(), s.AccountManager(), s.NetworksManager(), s.ResourcesManager(), s.RoutesManager(), s.GroupsManager(), s.GeoLocationManager(), s.AuthManager(), s.Metrics(), s.IntegratedValidator(), s.ProxyController(), s.PermissionsManager(), s.PeersManager(), s.SettingsManager(), s.ZonesManager(), s.RecordsManager(), s.NetworkMapController(), s.IdpManager(), s.ReverseProxyManager(), s.ReverseProxyDomainManager(), s.AccessLogsManager(), s.ReverseProxyGRPCServer(), s.Config.ReverseProxy.TrustedHTTPProxies)\n 98→\t\tif err != nil {\n 99→\t\t\tlog.Fatalf(\"failed to create API handler: %v\", err)"}]},"uuid":"2a985fcf-b914-45a7-94ed-a93c7ea9ebab","timestamp":"2026-03-02T19:06:37.676Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/boot.go","content":"func (s *BaseServer) Metrics() telemetry.AppMetrics {\n\treturn Create(s, func() telemetry.AppMetrics {\n\t\tappMetrics, err := telemetry.NewDefaultAppMetrics(context.Background())\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"error while creating app metrics: %s\", err)\n\t\t}\n\t\treturn appMetrics\n\t})\n}\n\nfunc (s *BaseServer) Store() store.Store {\n\treturn Create(s, func() store.Store {\n\t\tstore, err := store.NewStore(context.Background(), s.Config.StoreConfig.Engine, s.Config.Datadir, s.Metrics(), false)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to create store: %v\", err)\n\t\t}\n\n\t\tif s.Config.DataStoreEncryptionKey != \"\" {\n\t\t\tfieldEncrypt, err := crypt.NewFieldEncrypt(s.Config.DataStoreEncryptionKey)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"failed to create field encryptor: %v\", err)\n\t\t\t}\n\t\t\tstore.SetFieldEncrypt(fieldEncrypt)\n\t\t}\n\n\t\treturn store\n\t})\n}\n\nfunc (s *BaseServer) EventStore() activity.Store {\n\treturn Create(s, func() activity.Store {\n\t\tintegrationMetrics, err := integrations.InitIntegrationMetrics(context.Background(), s.Metrics())\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to initialize integration metrics: %v\", err)\n\t\t}\n\n\t\teventStore, _, err := integrations.InitEventStore(context.Background(), s.Config.Datadir, s.Config.DataStoreEncryptionKey, integrationMetrics)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to initialize event store: %v\", err)\n\t\t}\n\n\t\treturn eventStore\n\t})\n}\n\nfunc (s *BaseServer) APIHandler() http.Handler {\n\treturn Create(s, func() http.Handler {\n\t\thttpAPIHandler, err := nbhttp.NewAPIHandler(context.Background(), s.AccountManager(), s.NetworksManager(), s.ResourcesManager(), s.RoutesManager(), s.GroupsManager(), s.GeoLocationManager(), s.AuthManager(), s.Metrics(), s.IntegratedValidator(), s.ProxyController(), s.PermissionsManager(), s.PeersManager(), s.SettingsManager(), s.ZonesManager(), s.RecordsManager(), s.NetworkMapController(), s.IdpManager(), s.ReverseProxyManager(), s.ReverseProxyDomainManager(), s.AccessLogsManager(), s.ReverseProxyGRPCServer(), s.Config.ReverseProxy.TrustedHTTPProxies)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to create API handler: %v\", err)","numLines":50,"startLine":50,"totalLines":261}},"sourceToolAssistantUUID":"d03884d9-fa3f-45a6-bccc-e07ad822ef48"} +{"parentUuid":"2a985fcf-b914-45a7-94ed-a93c7ea9ebab","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01Cdbyk74pB6wG2GfhYSor94","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"Now I have the full picture. Let me plan the implementation:\n\n**Current flow:**\n1. **Main store**: `boot.go:62` calls `store.NewStore(ctx, s.Config.StoreConfig.Engine, s.Config.Datadir, ...)` → `NewSqliteStore(ctx, dataDir, ...)` → `filepath.Join(dataDir, \"store.db\")`\n2. **Activity store**: `boot.go:86` calls `integrations.InitEventStore(ctx, s.Config.Datadir, ...)` → activity store checks `NB_ACTIVITY_EVENT_STORE_ENGINE` env var → `filepath.Join(dataDir, \"events.db\")`\n\n\n3. **Auth store**: combined's `buildEmbeddedIdPConfig()` → `path.Join(mgmt.DataDir, \"idp.db\")` → stored in `idp.EmbeddedStorageTypeConfig.File`\n\nTo implement this, I'll add a `File` field to `StoreConfig`. For the auth store, I can modify `buildEmbeddedIdPConfig` to use the configured file path if it's set and the engine is sqlite3. The activity store needs a similar approach, though it's currently checking an environment variable that I'll need to handle through the config instead.\n\nFor the main store, I'm deciding between adding a `SqliteFile` field to the config or using an environment variable—the existing pattern of setting env vars for the DSN suggests that approach might be more consistent with how the combined server already works. I could add a `SqliteFile` field to `StoreConfig`, but that would require updating the `NewStore` signature across many callers. A simpler approach is to have `NewSqliteStore` check an environment variable like the activity store does, though the user's request for combined server support suggests they want config-file-based configuration instead. Let me think about the cleanest way to pass custom file paths through to the underlying stores, starting with the auth store.\n\nFor the auth store, I can directly set the `File` field in the IdP config since that's already supported. For the activity and main stores, they don't have a mechanism for custom file paths yet, but the combined server already uses environment variables to pass configuration (following the pattern in root.go), so I could add env vars for sqlite file paths. Let me check what env vars the stores already support...\n\nLooking at the existing patterns, I see the main store uses `NETBIRD_STORE_ENGINE`, `NB_STORE_ENGINE_POSTGRES_DSN`, and `NB_STORE_ENGINE_MYSQL_DSN`, while the activity store uses `NB_ACTIVITY_EVENT_STORE_ENGINE` and `NB_ACTIVITY_EVENT_POSTGRES_DSN`. The approach is to add `NB_STORE_ENGINE_SQLITE_FILE` and `NB_ACTIVITY_EVENT_SQLITE_FILE` env vars to their respective initialization functions, then set these in the combined config's root.go when the `file` field is specified—this follows the existing pattern with minimal changes.\n\nFor the activity store, I need to check the `NB_ACTIVITY_EVENT_SQLITE_FILE` env var in its initialization and use that path if it's set, just like I'm doing for the main store.\n\nI should also update the example config file to document the new `file` field for all three stores, and make sure the token command sets the env var if a custom store file is configured.\n\nActually, looking at how `NewSqliteStore` gets called from `boot.go`, I could simplify this by adding a `SqliteFile` field directly to the config struct instead of relying on env vars everywhere.\n\nBut the cleanest approach is probably to have the config set an env var that `NewSqliteStore` checks—that way I don't need to change function signatures throughout the codebase. Let me go with that and finalize which files need updates. In `token.go`, I need to set the store sqlite file env var if it's configured in the combined config before calling `store.NewStore`. Looking at how the config flows through `applyManagementDefaults`, the combined config's store settings get copied to the management config, so I should check if the File field is set there and pass it along. Setting environment variables for the database connection...\n\nI need to verify that the token command doesn't require activity store configuration since it only uses the main store, and I should double-check that the combined root already handles the activity store env var setup.\n\nFor the SQLite file path, I'm thinking through whether to treat it as absolute or relative to the dataDir—if the user specifies a path in the config, I should resolve it relative to dataDir if it's not already absolute, keeping the behavior consistent with how the main and activity stores handle their DSN configuration. I'm seeing that the current implementation ignores the user-provided `File` config and always defaults to joining with `DataDir`, so I'll treat `File` as an absolute or working-directory-relative path to keep it simple and consistent with how DSN strings are handled.\n\nNow I need to check how the `StoreConfig` with the `File` field gets passed from the combined config to the `ManagementConfig` when setting up the management store. Adding the `File` field to `StoreConfig` with a YAML tag, then updating `buildEmbeddedIdPConfig` to check if `AuthStore.File` is set and use that path instead of the default one.\n\nNow I need to set environment variables in `initializeConfig` for both the main store and activity store, checking if a custom SQLite file path is provided and setting the appropriate env vars accordingly, though I should be careful about where exactly these get placed in the function.\n\nThen in `management/server/store/sql_store.go`, I'm modifying `NewSqliteStore` to check for the environment variable and use the custom file path if it's been set. I'm realizing there's a problem with how I'm handling the file path construction. If the user provides an absolute path, `filepath.Join` will just return that absolute path, ignoring the dataDir parameter. But then appending `?cache=shared` to it creates a string that's not a valid filesystem path anymore, which breaks the Join logic. I need to check whether the fileName is absolute first, and only append the cache parameter after determining the final path. Continuing with the function signature, I'm handling the SQLite store initialization by checking for a custom file path from the environment variable, then constructing the connection string with the cache parameter for non-Windows systems. If the path is relative, I join it with the data directory, otherwise I use it as-is.\n\nNow I'm reconsidering whether the `?cache=shared` query parameter gets handled correctly when combined with `filepath.Join` on absolute paths—it should work fine since `filepath.Join` just concatenates the components, preserving the query string even when dealing with absolute second arguments. I need to reconsider how `filepath.Join` actually behaves—it concatenates all path elements together rather than treating absolute paths in later arguments as replacements. So if the user provides an absolute path like `/data/mystore.db`, appending the query string and then joining it with the data directory would produce an incorrect path. I need to detect and handle absolute paths separately to avoid this issue.\n\nActually, a cleaner approach would be to resolve relative paths to absolute ones at the configuration level before they reach the store code. In the combined server's root command, I can check if the configured file path is relative, and if so, join it with the data directory to create an absolute path before setting the environment variable. This way the store code only ever receives absolute paths and doesn't need to handle path resolution logic.\n\nHandling path resolution at config time... Still writing the store initialization... Setting up the activity store similarly, checking for environment variable overrides and falling back to the default database file path while respecting absolute vs relative path handling.\n\nNow I'm thinking about the combined config approach—the `initializeConfig` function should resolve relative paths to absolute paths before setting environment variables, so the store initialization code can rely on getting clear, unambiguous paths regardless of whether they came from config or env vars.\n\nFor robustness, I should still handle both absolute and relative paths in the store code itself, but the combined config will always provide absolute paths. I'm also planning to update the example config file to document the store settings properly.\n\nNow I need to check the `token.go` file to make sure it sets the SQLite file environment variable when specified, similar to how it handles the DSN variables.\n\nStill writing store initialization... I'm setting up the store engine based on the management configuration and realizing I need to handle the SQLite file path consistently across both the environment variable approach and the hardcoded file lookup functions that check for file existence. The file check only applies to the JSON store engine, not SQLite, so that's fine. I'm confident in this approach and ready to implement it—I'll add a constant for the environment variable name in the store packages to keep things consistent with the existing patterns. For edge cases like missing directories or malformed file paths, I'll let those fail naturally since users should ensure their setup is correct. Now I need to think through the naming convention for the new environment variable, looking at what's already established like `NB_STORE_ENGINE_POSTGRES_DSN`.\n\nDefining the constants for both the main store and activity store following the existing naming pattern...","signature":"EpzdAQpGCAsYAipAvZj0vpK7R24RqEHOkxS+UymRlkXoVXZ3hmYQrwUHidP9JvpLlGxIJKVktlPIZtoF9i41C9QTsVl/9Bu+1U62xRIMVF9t6ketIflponGRGgy50pBw7bjm8K1mNYMiMHtpA2IqOF6nL/tB73FdNo/ZWSwnqpwrPC5uD+twfVIMEUzYEHLMuROa7dOH0o1qZCqC3AEJxnXqqzaIomyDMDv7XH6/i4GpXeSijqRVNF/8Fq+F5q3SjRxmd7f9+oRDW7fqTQpgKLvZXZDitI8QtzrN3v4BGXoNHcIHU79zJ1540v7ipgfvGGB+weV9wWnf/bY1v2OZGzSxLssrNBqfvUS5CbAOt0fWUIaMPNwBaTMeLzw5O+AILN6SKdoflV+8ehlrIm3rw+z7EoOjAavkMc7XfcCby3zpbng/a0u7/+1d9W3pmNTtlc3nuQYoF25Ymn3GgpRngv3QsbWgCgvf5hsQ79V2QWWGxXM7c/64KgwF1i48e1BSnV9MixVi/EI0+AXVswcNj5ymH4ys9d0RwK6BwgS7LDj0885Nf8NLKFA6otVhekXIjtQ2kaAXxdd4J5CrxGnZFz8lKn/3gMEi1ch+dAj/P87UIpCSfvnSGIVNAQhmfuusE+PhzvO1fGlc7LsOVcm05wLdzEJzCCvmHjshwE+2CI0WYXj8KLjRRUSV1eJH/10cfa1GIUQqye6gYIWZaLSwdH+GNYNjoxaaaF+5xRkR3o+XaJ2kMMonPyfSbr51fwNILNA4mGuY78H6/0vjD49fT6OsHbYzpiX37O60xusowXSkIFZFno1iytKPPCi9+rr1ISBvOcIB8MFL7oOXoRm45ikT3gfJ3DxhOFCEhQc+t9LJLZTHM+gkhs2ZhenaGQBuVvo2zxZFXXdw3dAM+W8Wjw+3Omi8oUw+egsPaW12UcwzoPmx6nV2qvpHY+nWAtLMZIEko0xxxA+K1GuLiGQqomkSElm3NeaSXJ824g4MnUTgEWAtZVxd4/0LMvhEja7Rd6yZeXERrGRgXB+du1bW6SOhjP90AtbcxYv8t4TQOZuHnDZLwL9vgpt4B++E+tYJtUcmfqBWLkbeMCrSaeGJic9rHqYCCXWps+G7W1RKff1Waqkt0SDRIpGtvCKyfP57sIbHOxHP23Z5kMVqQDOJIf6JqNwQzZlONindswgQqpg8buJsPKMvfSiidN9H4yIPM0eqO1zEikeYNzpgAbcbS0nlp2fcZIbQiETJgOwthWcVmzJ9ogSFR+RPTr7PxpD+EDI20ci4fsMBL6YoHuPRCTedUKmA2DzMnqpl+GTvvZf0jN3e+NigWRZfYd888ns6E44qDg4SudDDJx9m+HvKUEA/uiBuKTQVjprehSkDntINpx+1eyErInpqYcvsjIeEi6agz7kcI3mQzutmtlIV0L8AkhQV2i7RyZUmw368I2XpEpEZR4rn9mASTqghwgd1ujzFJl20SLxLGcgJ2IsdAgvkqN2sVufXSW8L8ZeLa8+OAcvSE/WzLDc6QU47p00VkiTtRWN2lev7ir1Xyx2MRnRPyyYPb415CN4RRmm3pncxftNM1i5kogQ2XMLB26suQFuLRC7pX2dmDL9uB5srgazyvK5a1yQR54a12MAe03mUd9BK/F7cxw1oYolQJF4PbNEf0BUnmToHn8O5q88KJ2fQUzEyX1XfDKMMus2jKKmWnTLDBULv5OCV5TNdrHH4hInrbkVk3gAQNPZy+erMy9c0UBQjVGZ8WxQovesGNciAAReYIGZFpQEiMH/MfgckFWA4S2vIUjfaJvQLxTjDxtL+gO2QR3sKww/Ot4EMO7em5JUhYlADq6GghveTeDH5J+5624bmQb5jlBW4Q2MBW5iywmm4oPD0RVIZGqVbEJVtMT/lYUnd0cu7kOc83B9/sglNViZwd4tRjWaedATgd++8/AP48fqyPEN7WztGBPau+KleY3tU4qnQDhRkhy/Y/cyxXDoekiGMr8fqGQ22vzhR9fhfhyh4gV46lb0LpCP8VynSpyHLG9lAk1ENExDtexrBvhbi4kcNZLlhoM8ZO9wQIJW8akeiPHsGO1i+Vfv5V6GTaw4v7RC90pJHMgkOt8HNO5YNywvJA15p1MBLG069fysg2NIaMpb0NuKUGqb67WfrSh33Nmomtd7UlZ0b6msBond6Mp9BrMCWVB5GYG0Bo5rLu901lMOG0UDgn7npv6bRtOSoGAIMyx8B5FdfqBAs77RaJdMao6l745aFYg2Cy8AAlwesMEHE08B4aSrZli9zMnRNX7gy4WlnnwsFp0Fx4gbaU+GzT7JJ8yom1qWWekdq4uCzuw1997sv6QnQD1BudBiN845HnvT84SC4aVciiihShVdWkT2VtyMSVlN+x+NykFb7rlvbZuTh4FGxLq2uuikTui1t19/mJUl4JMFqCHUwauVHWg0e8Eo7ZcsszrjVNtYkI6ypEE+dsDn+nnS+9l1V+eIkjk00sZSfBXzAHDQvacXGJvmG701TvOBHZf9qDICwGn13hUHCN/HJ0rBsH9/dG5thDPiwNLAXvuMCN0E9y+DO1LeLGD/0KOjBavkTSKGqZMVdOqAYCAnsz1e2BxWQppXS+vBC8bLUetr+GEjUKgUQwGrcGgf5eQikU5XO9ZBTxI+vTUXhLhC98H5xeFX+VedDYhRLIfGBW8HOc4B8VYtH1tr2dC459bV56f6tcwG1uF+nIOTW94Py3FlxUasu/EJXJT/0kq8wSSzVxUyoPcC6Qe08T1LRHbMrad9AxbnZt3TzC1ATdqsmT/BT9uWpMInWKdLDQllq8MN5uwzBZU80MY4HRouOPKQ7iHg7vdPNgtGivgg+srBxRhF+XdZD6KSec8yrWfJwZ314Pl36V8IDipY3tK0wYwZlYu5BR1fi17+DCIeLpzmyvD+qsxLfg9mo1ftbdUvjzfGKSsnGHm5ruPioCOnoIAeRI8/gJfUNTq7MIGBi5b2vaz+ua/2T/GTppTCBPQgTR/8q0joTQNCUNYNttW4GGVnEPxZXYiJi2bB1yjlU3yVhy7sFLkRuWtq32XQOz6o3Mnn5Yja1UWhTEfBYuN1/Ad8y35WCUOYQmelnOtdQKXmWAmHGr6BYQYuWvBI4btpc2aJgePEj0h2XjB08wk5dEBzUCHYS5JKEh1hkkJmOFz6GXwz25sXLe0IVCxUns3xwOEIezTrKNP0Qm1uax1mMBbc0VVdZoR+yOUD9iiHrVyKGs+u/hGswGGAWBbaEw7kcBtxgXrOO7sclyixvIDpbkrhZCqHmqK058X9dEMASuykNOQqJL/UWnVI4agfVPrrvTmDbioBxHj3s/6GHZj9x0BWaGoxe6cshYl67Aaw/rkvRNtqVIA+CSNun/BxW5FfufEk+SGJao4QngqKyL/eSpDdrn042l7e25UivsN9imSRnyiRUiAu8PLxSAqC1L9gvKp99aM/dfLCZiC6L7fUEHLysCTk9CfxU/ez0B3HL4vUYAbVxSTmkRSisltzLNOFVb5duTAX7hj+ObxwQpVi36vS91DYpJXxYOWccbSyRn3qYcSObOPDABnKPwI1FKsLSfnM5gjsPiLk4LaUt4JdpRteO8loRSHFR5+V7Ii47vVOATc4oiHEIvkNDD+soBgNTNHWs2433SZZ8hwolMlUKOeCX1bk7DSxay0YvNGUltlYMCmqI87MpCJcQ3zEa1ZC6216hXlnmpNDRH0UhGSEz5FtgWRImqzVuqa/EeszHO4S+XowXOE2LnDTbiD1nfwDJodObv+Uiy1YJn4SwRTNkH2r8vYBckBtG4LW7ZVhfgB/IBCi8b5w+PaSrvlW/5dRzUTjAlZTAOP0LyTvY21+BPRJGL9Ry4t4RKiuvdgfKqhbhWzezvHi0t1r/WFugcATop5Ahclr0oD4w83rOBtSyARUb/H+PCXMwiSyy/lRE8sdrTOyRI5Lab9mRStbJKkXD3nEzEVXg40pFoKmlJf7/gEn+DjNiE+gOAkyxRGD8pF24ksFmnsRMpqw/cRuhAJ8pRLc514qjlDf+oEpwO0IFuhxx6mdy4dL8DcGtD3jcFbk/jeS3+RtcGWvDLH61vKqDhVB2raHLgeoghyRJVVCrOCkWcfQIrQnnXgmGXd8SoK9lsxylwXYtpXHOW5USQWbLMooUivpdLcdZyoe9BFmM6E+GfVropBiOpkqcsu51lxpRo/O+p8+qPGK7QPeT6LmyYea/B3gbdW5wE/NgvIjYFVTD6Go7F44FYq8DRnt/4m/x4y9LER1CE2bUxy08Dpvj2yj8001qjWZcmlEA/Kgo9+jm77hjpR+MiV5bVCtgeH8Don+aBJJfFQmnyelz4yHm4VcSpEabKYdoX3u0Xtlq9t7BahdzuXfbEljwfp1JiBMZFFzG1FmCUjzjuPun4mw+c8RilTeurzPc4BDDqJ+1/US8ewuyS2bteV0BNg8zGYAReihnYRAZMQ3im7kYa0MOZdsQWXcYCfyW1wIyfn0YjKIkoGRfVcYmnFzr37mHTrcs6iCaSOzJEJNjoSKrH3GLGRNG0P5gRJ9DlNYCiWS0L1dmeahZ+zS7hAsrQn9YEQ02Wu5cglALjaNSCsk4muMJDVCCRd6GEdqliEDhaPSYboXSPfSD0rCkG58Jh5TDbqj3515kdTjZ9aJ1mZ6Hedpjz1ArqaJNvWCwh3Z/rs7Xg+wVfQX/nv5OYURqK2gewqn9AlinXJoSzOkMWKOdiLDubfFNEwM8KukVJoqUDDFnYMbzZz1FUvOFYgcCPYD4/iT3wwCtN7CGZtZ9uYyfbbnNbu94lL7TYEKfYF9CGotJljEp25X60MLUJYBnIZkJFqN00izo31CxAtDAnjjeWR+vGkGkvjMvALL5+mKR91vjUeXuV7EXaaC6bRWAFUT1tIWd68MSFbcmqv5bUqIewiBn0Qz9rZAJk+P9MLF5qWNSzUvQ1gkLZwCOPvK59BWmrp6w9ZPpBi3e6550XuKeEyfGaIBYyqry+J7kgCjMdcYSRiLsWcmVLugr8qJ63rahggkZg3zVCmey886jbV3cT2e5Iza6UiT8Y1WakGSpyeD+YIZWQylDmoksD7bj/LsNWhRCIof3pRZIZmpg30G5FIT5t9ern3AisytKkek/g8E+MHgANrVcnaGKyoIGcE5g49lM9bGjwe2JUuokUx3SmAd7x+uTZEEr4wzU5R8zIpl//WAYgiJ/aa3O6XCUVqkVVYdYSBoWO7nJ3lQHH90MzPQ5cp2u5iR1WrDAAfCDstfybECD1BYsqhezxo7+k9oGU/Lz73ity2vGarPkP2OvAtHdHZ6sPRhBVtuHikdELtddFscG/J5WV98ApKDb47hkesEvUrVtAeKX4CRy1R/rAeXjZ8eeD1YEAv7W3T4469WjqIZJrnVshUwYN46qRTe+yZ8glEsUsHC0DTolx7Cma3P9Afus2UFrAHfC/LtXMvUV5gwYCAIFE9DCncZbMOWBm84f21CD2uHxquhBopy3nuKTVleUj+4kPCPbKnbz++9coly0MkicZXZzuJug4nRCXLcQR72fK8HdGZkEms1mRJBdf61G/+GAPaqPlAMS5HZm+G2a4t6aKQKJg0LNVJBOPwfFXX2KVoT3TE8an7b5EkzR8AxPF9GlFwMWvyP7f0eGYc5b2xGw2EkJtPtzM6Vzc77h7yLhRecUz+heWrEN/UpndLYGyeZHa7/FDbFfEHmDz9k5u15nGdYzNlkO6DxWAkXnRR/3bH1Lb4yemmin+w4npjLbQTHPPlIvLzmgn2xiiJsK9GPXQGFiD7Jh2H0xyUzryENyBslf1n+/M03B7nKGavm6v5xpsEjEbeEmDsLL5+ieC4MWzBuaaWaw4pzKmCU5Nx82cuSvN+snivMK4FurwbH/tE+ez9q6SHWzc55H8slxN7ONYD5QYOQb6St6oVsIX9zv+RXGmZiYC7NmSPylIovpY8fHOZ/fWwWca1bs1ygT/B1/j3swWC1Ht0aRzpjiXSq4Jx+kw6bPbivOLiDE65mBo/JzhicJDkXSHeFzgeaRKpAoMMvstDtBA8syFKtBtL5yLtWzSiEPAvOQenRBJc27sRuFAHIqRaMbuVgyv0pQfrhDjw/H/3EVIMGdFm2cvDN5lsBxHwZH4qC4BZJ9TEvpOm3s4r2EJX0M9+ghijhMz0Kaht4JTmMnAXrf4MFEjpKYx4OwWkmRFJcR41SSHQ+MaCSAe/qfwlgwJIWL7IUGpZEp6iB+4dZ81FFP+vmPtYMY09R+bLjzpKQIzYm/txPbpnkxGoi5+m5ymu7ZAnriHF43J1ClMF5mo0351ucZordXjCgbeFxkR0w0kCqDGkwN2TIhr2Vt6aeLxytkP5cB/AGuWXg29wLouTu310+IGLW/1gTJOqO0U/D0WrcCG3qgvzGu4MrZjH9Lyfh3i/CPn0tswAaeFOjE/dNPawMKw4m+Cr5GVdWu0xze05PWNSVLJNh15lPbrpxr7eq/R70DVCxDn/Sn3RvG/pagBFxSD/OreUp4CC82KTtefS997TwZLa6Q+hvabl4ZRSyJssrdQfPEBNnKbZAnZkhAzroJsttubXT96vNLYN3uVBkTJLdh+0plSntzFB48n8wUIxREyxBrWuJHvmRySv3gGeQl0bf6ocAh/r2GmMKxkBv9PsHxmFQ2PKxxZjEM8Z8AfvGaSthS2u+f2nABH1K8KWJ+FXvqkvPKH9uWUWnWI855WO92dvi/AalFFivdva84JkveAfm5hydUcAqyRp34T4belzr50vWH1xW8W+cUYEENVEdCxi8U9F6+tDKZs7Ax3LbVaezEjvAlhL8iL3XTwD6y+v0wlFy2XU5U4n9HIfMQS60nckOfTTFqUxwYKYGFiwN9xjY7Jw78PIZxlpU9poRu3qaDw8s5V+E56Mnc9YD28NwqjeCctZJ7kWj0qsNbm7a7U0GjAd5Cn/BFeJ9WzwPnSlcgYukQ8FSXERZXv0tVSkHwdpzF6jIZMGsrkevWmlgrlKfvcjYdNX+Xe8qTxO5V1ehMrj9hsj4/mu4K5/GdmxE6IOEfOBbVjGuSJHB7jKj2D95E0LtoqyqhO4iChTtLCpqxwGo7X3bNv6oiLgZauEptYV5TiOe76kbeFE2UFLVaToIzO/bc+m4kDatg0jQFdKpJRPZFbTP9HWYMf0CxeLRMflik+831P9nQPg1k332Ng/zXKsEj4UXMjQipQHEmtyWSnKsVdNR159CnAKXpGISeVFM1DRBl2SlLkLwL18auK9A/7mQw3ghcvjJEsjLiO3ERgy1sPOV4VAm1KZi2sgWIfZJIPfkQDgDGupL/eYA/q08TOhIsVGVhDdUcxDFucY0nGjcOtH7N9gV7XO6LJjCJSQUQE2OR2H0EBAy5KDgMHtu8jLFKXQWZSvJLPKOLNpUXl8/0TcKoPA3KMRQaT2OGPskMXLk14czQFJ/sRD8XFtjU5F5hhamw4DyEEyiSvwk5xEr7H3dQCNQUMjK+VkV1JkXOXp15Z/RbYFV7+veyKeBAgffkTtksNX1ME1pDzZ4mHtT1mYl4cX+VWmIm3tpPuOlX+iRDAmFVK4EIcuNhH+06xsCUVGnHfj91IrZOFWnXK9UfG7ETjbVvzAxROVuwSQ2yxuHU9uUBZ3iaU9zDXDx/U9TWKCbuG26FrEkhzxKkipZK/KsuoSOzMwqyfkZ5xbx4El6xWyTM8ABot4u0hExN8tQ8/fTiROwrQ+QjAqGkFfDeP6MYSYHFirbd/v6bODybCGfNkM8XjPH/Igyr2I87jYIdL2DydzpjpDdNZRA9i/bhihRUHCCIlq9zLf8q4oC80cN+kphqYq61Oa2O7mFg1ZsTD1EaLYNgX4uzYVuast9uplyZLwS+yaPbVH7VTwwQinO6Pbf5+U+dfBzxPheCaIIto5XgISvT8frmZ/NfH58HoqSauKTbE44F6fW89HUi4lw4JIS99KIQfHrzulXKrr6jGVut0xb+YW8C+jkA/THx4uVxPoSgUN85Q3ra1cOFJXZBUFAwo+ZhZ45KYR+XqcD5A12IO4GMDncs/6pnEwDf4jTBZn97KWL8Z6v0jfwYE4RndfQpMsA0cgTTclxWs2hRSC2rqCCEULBdQ/y3pBIqEsT3CZ3iQfzNesHQn2lRnGNimOQaQL9D0rG+pesmglh/idQk/Mi5YqIh9Yd76GOELOwNsvB22AklKZ/wyexHuyuPYxhpl7G/IuxxmL8bKXOhHd7Eq8/XtNpmAmrkSP+/KmQV7HbXLHbsPoEo27h/xSYsbeWTM5WMVIYEQuh3E8LW2E/idjBknzGwiPZjnl5hJ0iRCxwp6zH35mRZXF2A6pgU9BSCzPzG+qZV5HEpZMq/1Aj8Jvch48nRjvdc7iy37O3GYn2+63u/YbWmtiWspOly6oniJHO2p1LTrUylu7WzdxyRET+DKjuQmcxDhles3PLM3Omx66OCqVuKq9lqkxxVXIsMF1/MmeTbnIRqCMKZy736cYOxIxiAJT9tiEuUZq4P6LCsv1kMnbYu3RbloKkRCHQ3Nb8gqvmVQ1OPNU/Vvr1cIeAzVKD8gZPsz940Ji0t3GQEy7DcSNu5M4dygVUlkfpMnlOeYXFYf+rXgLXy+C+zJqXskDRKoolJAkOF7Cjb77JF6eczKiL/mP2SxjWg0hehASP3ZHgvINuSVTMi1g9GSoxhXovlBI4PPUavoBur3dw0KeCrUNnP5/U6gcGHh/iqLgUVVaK/R2s4iFOrgY+uMegfgArhxck2bvsqO9qd4yjZWN5kl8MWmd3JqCr/OPcgSf+GgJCNMM1t6Vi0aWUdLMmMUTZMexMmIy9sphBhcrqAwEQw3iHz7jLkQziQIT1M0GYJA0J9jAHMtRNyhh5XTIUX1uVXRdjjWJhWnXVyR5hVMrXz5OPIIbEfIyDAwkzPEyj+VPsJRmej+AzTStxXHJkyI0S4SZrllzYaLp6ah7zsWOrdcck7Y7UII5oR6vs/jAgiGMBV4oSSQ3a4CHeRkCUwQgBFGvpRg/ZYUCJQhASE3r9QRKx6OBG01GNkgAT8nWNoI5dwTIlhcCjOOLN9dxEBvHhkVXthtJn7XABD2jziDVgM0EiSbr2Q7pdHweTZTNwsSip8DHqw+ELuHrwu1YCaz8dpy91/wPFt/L0l4aAI+wzcVeQPUGe7T86VvxP4sUq/9C1PAF6h8QSUiW98ZLZ2Pasv/LRIY+IlkpHSDSyws9Wl6wL7kuwjI53lNbQJ0c7gvIl19AHlZL+F3/3qrrB75bNuU+TUBLTX2re1gnVW9r1qMU/CSO8nekRRAvwG5oRBhA2DyJUR/xVVYbs29Or3eL8lpk3nC3qNrejnP0hSXYq9gZPxNjV72CCqpJUmONOQPCdWXcxLjh7uPCsjO62Oz7rtsYsK2alINm0s/f5z8j3GyVxt/ocyVjVO4Ah6ull2LAUGrtziTT+5C7fqjRmbwgKvCQM4X8aZ/JFfQPgoi9aPR4dSbc+IpjODsQj2pno64PvQMnJjjBA5mjvk20J5bT5muhFzDI2gssG9NjUdd9nfkAuyggvyHV+Jri4A4pKR5WAv9zpcigNYxXgSH+M/DCpq3zgDocXWbnDmQeiBzixo88vQB3VPs4kGILBc/HHbZx7VNW7ed2kyT4lDZ2MrZIEACJpIZfOMz3Ag44/GfXAIbwB2F3xBO/yGEkcSSDq8UAv9vycHvp/s9SmE1+NsAGJmGBmXW8QhtO3jIQIAsQsIg3n/jAl1BrRJbHLnr4b5+xOGi6dzT3NGQOA4R55LTadyOSiVcpiglvBemjX1zj4W9miyMZg8aRtuADVvuJ3SINJsPStY4Xv3JQmMNgqlzuW+1mstQcqIk5lAr0Y/+WzQ2mOXO8MKQ3EK1uqleMQSM5irmPBQFQgd849QnF9GbJOYX8TyDLG1PfiEHNBYmIhqypTEcftnewy7ZDqq5dYc9nABUX/OzoNZlmr9Clw09Yhu+9X4mgtvpDn6lbKFGFLzJiK1ogzISJB23M5P6FFc0LAgG+9gIFM7jkcPN73IxbCT+P4kGDvPU9oJ1fYSPr7Imyr+NkFsilccRfrKYih/BeHC2ho3LN7ahLM0FJPUDRa+MUROh4dPjSrPR0RrtJk6ZHTuInIk1P4UUOIxXLu2lK7cHwctne/EaI9/aAfN/SSsqxTd/F6PGHFiBbNZhJYorFz/ll8vV7yhhtvPpgoLj6OlGDffyaVoy1OAS1sH5UqU1dfPcQXSPn3PvRmf/4/etanVWDEXUO7pDyF4BB14Re/IOWtInLIcMoYOL9VUrQ62pOAbyQszyURm3pBGUmJ+A/QCPGLUHgmFsSPCkCbrEBrf18xe/AC25T6N7c5nFsehJ0DfOyhdEXOAGP5Dnx55M7/EdLI3n3HLM8g2rQMMAKFOMEAf5koPcl//cX09HxWE5Mnd3iyFzXSoCnLGX6mcBFzV8DgBb30AXKM0/OHOsWzGwebWUqKX/e73qfRhuKQx1AsO1IH5xbKbE+RSPJftaY0XN1E4zXEDMcqzMqeqdQ6LV1ztDl/7AUwCcgGjmf931XqCMB1uBBDvkgNO32n3jlYclt458Jr0PjcLiQYL6FujUFZC7ENAjqovFkSHkdOOJwcR81cmusT1VJyBET8EtRg3y1VvmwdjDmD4MszEO5qN6FVUvU692edm5Y5MIiihtVCyhlDFPAT41bnm+rUvYfgIzcKq3FlrWHPttGen99oNLZiz0qBmWbKBB9IrxjNmnKDMsnQWrh+dEhpJcpCebPBNptgcg14ygtDGyIwvtGVVlrD6i9Q24w3fxfM2vw+0hKM3+BZybPra3cDijzDd1KjT0FhspbEWA26W/vhhAEa5YWRjk4TVZ1jqTQL2ddH2YP83E2uRPIDux1c1kAC1DEgV1U2WQwVg55q4Y/AHvE1FvXlkZQ7cq3xin5qWuXfHMmuROMEO/YmH7S5gOiZ3VC0Uy46mSrpJxZxia6gWm9D2JYOvG5YgbXV0+IlzJv45D97XV/iFAu1EPNkIgO/G+AbDmkYLlvnBHZ8CNI6ahEB28fzm4qrMG8WYnoTV/sC/2CdR2nfNsJVuy/0iL6wIG6lqEbNSgfcwLjf1bDkgYWOav/iptNrvYJms2WpC+LbFJf+VMkxj1LxZSNdirIY4k7/CR4f7FmH8Mx+mWGwdL3Pc7HeV4+LBHBQDVI/darWEZ+DDHjCbIfV8T+M9K3mC9eTqVf01G15QATOyXDDhEwUZRDljEtiCCcEcIJy/SXT5ewhShjIV3TLdnxsMtJoJQsTF6Ur48Y6RDrmugWEcb91vDvmHvvMIAB8W/JskJZXUp/1rGOnw3rl9T2j+dJ3xgVZ2ehiGyzeMWt63g8Vz3nnGPLWpyTrTaJEFFbo7IUriK6c3b3wN2y18EgIm64WKJDg/OJIoZdCFgr8B5WifbfcUoB8UUL1YSbRKDanzoPknJi8RaXVw1YRzi1Jwal3qU9+ePGD3gQCMmljLRc9M1ilBR1x53GQ2DLXqjcHZ4HHhE1rZbkxWtq/Nk8NCSgxtiltm1PQY6gSm2sFUWKls/CN4TcP42utDnECs6itiGdBebM6IeJfL3r5DB6Wi+BtiWMkQa9WB+sDMyytNmkzUGA2/fYouPdJHLUjJS1j5MQDUnQt/XVIUoVKoy6LBh+4Xi6yDnYepddi48QDDLl1IBHjZVGxER7yek7FydYta0RAlyIxaQjjpnhXDdZk3ABN0Uf+uI30TvQo6FkHIb1ykD8hIbhH9LRyf2w/CZsiCzG9Cbney5gIwgZ4es5pvZ1/r0qjH/qTRBMwOz4uTr6pGVNs/g83lyrEcYVi74LT48GrpMXydy9MK2/XySsvMpaKsfe4Slth7AvJbKSShfnsrztI6rDjHNfDLBcBUSdZ8q4rrS+BXv9NtrhOjSBrndAcGOfS3hEK3v1WaPG2QZBJ0PkFjuKrxeZEUcSjsS8yUV6eJ1ymJcAJQczYQCEQ+4W3UO+E9cnqnABiPjlZ/cJxr/gmptoF5in1kW2/sDfvSQp1HJMWu2LQebgxumEKQ8dtIDsmXXeyS+U7BwUYHqiYczu7HbZv2s1R9PfRo0xcejukcVbgtERh8R1EkANvXVxuKYvUF2BeNtrKBxdooes9KQ4w01ylV0cBsBFww9mK2lCYHMoXJcIiyn+Y9RBHTrSzDNJj79aiIj8J7xAERBCzF3aTD27Eapu/yzxD8rRjc9UdKIcOS6wDXQIbifd7Otia5+VAubGNJP+7jAmzWaqqR9M5/Z5NeuSco59su41mytsVxVBto/NrMs1BMvhax/TDMpeMpNB2dRPyhV0qKN/As29RGP+66KdNegGI3L0gyoc1U05vE5hz5W7KpQh53pE0oAv8HP1otkpR8V+kDsTR+HaDzXBYdbHz7j5gYOoyjQMuxm168Cc6e9Gna6zuUw3ZGTn4psaCtlCyXkSjsSY/nc1E9dLaX/JSODMGJaJVLY1K4jTSuM7XE90OY7l1MKtZTlYGL/ICWiP/wjW6Jxu88wfJ/Ze8BEW/JJNkyu8U++Jbz6nsUrkVudpV5VRCB2zn2JEgsYuOWllzWj8ZQ9PwWCElSyo7rLjCUlMoMK47uyvmSoZa6jIBcvSb1hXjq9uiA7nZwQWw6OqIfBOGdMIxS7otVcRQ/h8Efe6L88qlDTdIrTlILRYnPoIbI5WswIYk+xmLiam12rAi0e0QuTirhlq0U8hdEj250DysZVFXSifVKL3RjQJ0tgqFlhifYW+PQPZDH1PTZTCI1nWCNcCVQZoN84iqgItSrbz6wmCHE96JiWiRpcVih4S9oB6vxkCjWTjA/VA22+b2E2D+e3aKoxeow7yE6dNeNCD7AA+d+cbOtOTItbNiOHz/pX+lSFGslJE+rERdoTEod1ZBzU2JtpSwkL5UPUsk0sd/+szQY+MnxNecBphkJQ3aWmizhrtnrgoRzAa+0y9vdK9FRCIdKKE8XPxe8WmJrHjtt1I1tgQ4kgWmEoUm3/gUqyuW66WBZInatiZF6TiKyh3Fh6y51D/cwszhOVDgrIC84F1REJAtePFqlgsRI1NnXtJ+zOUBG3dTpgIiv1PF3i6v4+bLr8jP99K7lITk9B8ZVv2kAhXgVxZDVp7ckGflWAapzYlK0IWe3RlMAK+0pbJPaxWhWlnX5UK0KbEjPBVr8Fi3dvOGXdd+6iEdDMYlhGy6b4FMLx4FudEeq+f1I5MGV7Gp7yU5+cE8yMnIRulMJmQNWHpn5x8uY8vhQzAR8ST+XqR1vSIU69kNqDUhY/ZxijTAKh/iNNKmECgCOwTnZAiNGnds7czj1z94v2T5grHTdIpCOevh5N+Of5WB6mFWo/LTg7ZR7juc3ib+rivLOLbp2OrEaAJoJasjfL+MASQbCumrs5zs8IdHNy772w0mHi99umlszn5l9/sCaGlrug/So/2W5k8KO+PdS6IfV3mdsjBPnPrFpC7uYxVa8rBO9t5hn48/dOJEhPDyMEYiRNTSYXEVQwt2zMnke5QRkOLNvfzpe+Om4Ze87Xz04qIZ92Y0HfKT6jB40A/EDJfL2tUCaWuL7NyApsHnNDYNYkt0LIKwhDuUvuC5ojbL6TXJ02YKlUiNp23+TcBPxTrfgnARfBsYwfPq1PdbMnrfXsYycPy+oUZaoWoLxq2jNldFbbhjhkeyrhSC0oZxpXoeiVnHqcA3lz6GmEW7wd471IZ5rGjxaQifA/Yk85ic6DnbGzMyONoVHbxWD8AFqNa1RNS/3+pnEloyTtZim5PEb69I6gPPqKCFqIWAM/p/GZZSBrJbV2X7RdgMVbMIKHFGncLSlclz9SUdShxbM+Dtt9MGDyfjSKnnbZej0vNfMuzOv50OWvRDtWkCl3xiVMU+y74VXn44ScP8g1/br7rVBuXwor2EQjdOLorzkGA1AsamDLd2ARZu4eMNDiRMGBTw5i/AZr0wMBT8gaHr/TYpSGwjboW7GVVv9MM2MRUkkdqGkKqOuXihSma17wdOtmibZsFvBRTxXZvjKEC5Bsqr7iOsfyMkC0ncplxkPEOaIFHn/ZbmFaP2R2+btPTADetw2Cv/hFXYGDMqffEaRbV9vbBTkqCWmYjuad88s/Kult6H44vlFrC2vWMYcuCcplc0Ll3C6+vQ1kFQ20a0rat+FmhLRi6GHlK1Q99IdtT4wjzqAYHc5NCTBgu1LuPq5s5dZtdBUzNfxko0Bb+7Y8pj0NeFZtB6G/lbM+cuFWfdGGpkgkg9DAp/HITeaFRLjoa5X3NcKd4jnP67HGZhwvtyx4BOl/64inMHTvT1EHVVJ3MeR6iR3xGtLCMVc6RMiZbBX6MCZT/NOcxKEP3et//jqGlnifC5p0hvApZiAjguAnjif+I11cKMtni50N+WrwQFfNhId2vb6VNzXy9UJZD5D+f+YPFNRNJGCaMDIpGUfUsyElzXq4kjeeI6gJiUxRVKPROBl0rng095zv5wYw+I6y6EvXBhMCBvNJfYrqF51Lz+5XbkIm1ptWIPm/+7KCNr7wRArpyYeYVWoTSokwfNNO30mtHk2fpDzk9w4dfVnktH22V9pYBAuPNPR/wIuk6l1obJpRwZwqWsw2esB2TDpwq4NF0KgbPnQ2nb4GUq9YgLzcm5TOSJy6rC0ATyTIGBozJKHfPbJRUWUYlUu4rpfXogd9OAjglSKy3JU9q+iUjuCFr2H1OHvR8xK3ox0pctsrlalGl7hBjIO2OP6HvHGLR4cAwHozkZFuzJL9wyigFok1l/wlWfW0V+K8GHiD3ho8mnCqy4es7OIsqQCw8tbr3cf311kgWOLN7pairqsmygzU/yV2mnlSdpdufbjCaGbA+xWwvn9MceL0YZ/mWDse1AMWyq7Ku0itiMiyUBiBxx5xCGOEok5CaHFXy9sfaMFGlEfrSSjUJgm/EwuvDlNRjsUbRKOGcUzGTpAr7t7eThlmJPU1YGpv7mgvJu+LaoD9NJdu3FeTJxgPabokDGY5ijNglEiPj9IQAmWqsqdcPBGXmo/icoLoy/q2FP02cw+ZiqnLhhlgA6RRg7EUC9jNqXGgPEk5HSGXoi64fJkSXw0b/DoOZaOqKujDPhRJz6KXk+5dvKH2kvVOgceadad26OmjTw3Nfx0y92I1oUdSnssuoUrGwrQ8bhtCFiiaGctCFYH/awd9jstLDUosPnI3O4zv+0TJYimebyiktBeikYq3TXfAzNYiip22z20/dI8RiOulEgF4lIuUqUbIQIZF98YnZVhVuBjL6bKaHvOD0vWp15dFREJ964Rqz/PU1uSOXrGwvTAStu6Vw1ITzbyiRC2q0GHrT5CZ3iY96JaO8zCJrQ06Vn3AHYTiwfzk++ojDOZQGFoFSng/Ri7cHR1BE+2Lau1ddSNtAjVWCyh4MdMWYwx4qEo3g7VLw1gXljzq7DpPVhR5SMLFnfzIxrKgKXPDgOXFU9EXNZxAONpAzu+0IbmyvEqmLO/NGOPuHJW6+IL83yjblwdpl9G5p6UrhOAHZG1vuqH5YUr/GA4Z/4la3fS5vrpJRbbgJkjtad/sLKeDVG2aB2X+UyfzHUuTAAe45qLXUxJ4LdZsKLPfr5JZTnghPARV63EGFXOGnY2exjTX151OvqT44YADkpA4WYnGcH1T3Uj5kVJ0Ta2Co5N8LYhjv7BfyksmaKnPyPWREy9CqUt/hTV2ZebdzwLw0FlLCBE9gL+xTqonx1kpSXxhDnmnlBDRCYoxpL6Eef0OAJJYkFdA6mMpSBOl8SkDgdeJMY75MjkpTDq9CLkH051Opekl0CmUYB5gwNMAhCgSE3f6oXsm72YaKOU080dKnnmmn+wI36TmGHmzelX9L27pnw5hrp+qQP4Tl7IINRRReWw6aDsFwQSBGjsBdglbTqGdG8wAvyNxCnqs+1d6a2llSukN4bglnSSI3EVtI+7QYn9JXWp5NrWGXZ/EwujdYrrE/OWBfIZY5sdtD9Komivf+4zOvPmOehh/WORQUOvj2M4W6ej7aezpAQiASq1YZpf0dSZydoOgEuoJckVZU0w5CoHZNgEoOxK7RRStFvGPVO0dqPvqu86YA/cm+7NGqJQTWcrCbsGjGAJiGz0H1BmyWp28tcl1KQjq0kXdTbovS9kG7Spqet1iHEMaz6mjuJRpj69Wlmeu8N3UF4kdiasg9YGHrY0uFwNKn7rr2C7C1c3/7rIOssJGZu7fdSit3tcC0LLmIoECfHnjB/+PRFFRdGnFUumNc8K20rhR/DSzhoFeYYN8hbsBRiYJFGrPLs/OhNjrf8bs1k22hsYGVHNQ5YdLyIrSfmP22OkaGDIAx7WPGrHkV+NUs0fb7SvvWOurG7RcjeLA0XiU9jcFcx38qPYGIS0mDNMf4eP4V+6GRL5Ig0ubmBEGN3/EESvH6htGJSQ2EPZqqiqQ0v6n8df5itEeShChJR0H8BQg/9du3zGEVgTz2EctYNfU83dA5VrREZy64Ctk7+7eM89RrJSY77UMqw91bYdvxyy3Zk9UOVRtSUqzJAWdZuhaM5b6g6LG3oHPfJY7nCHf+QIN+xEvCb6ElpBtFH0fdjolPg+eCDjiHFKx7VcoMSvontJJNnI8aQ0ltEtTKOV9qye4RW/HpoZn2Ol3b7SOfp+oBNp/R5e1cIG6D81IpeEHQM1LndLrsUT9MUlpm0E0LxviJVYS1lj5Y5GZM25c04NYwELNjk6CFGeKEDbannr5vfOmoPORQHJVPw8GlqmWVSle2NPph2s0QgXslH1dejivJBd0GRmjUiHOnbHlVdKtNZpJdXPnimQ0rBd+/M7I0b1S85BC6RJ/74kTYqxi0nnvSrSADtB7kdcyGXQ+w/fLpP48A8WmxCJq8trZDZT//EH+hosNkcHvcpXzWWVE9N3vs19mA6/lDRe07mpEy58/yQyjU5AlcjVvsh4czCg9dR+u/ykn5uxQ4bPsg/8QKCbUmoBQZsiiaBLOXquZ8stPwuvjWL4sSxUqYtHvLMlJPkd0tOxeihH4v010FGpKs8nCs4U0943bb2XV8gHZIBG9KBhM0n41woBaLo1b1Ov/mLPur77Kn+Fa6Z92YP7cH9wADwBrpqLjb6QQcwuwdjY6jPT/zoekPlSpuWgWQ/3iWJkj66U+shWmyiAMGl86QFXTzMS5rcntAd+GVJOTkND0ZTyXNiOXVIH6gIsb2+W54CNDuR99BZJ+DWOSnjaKwuxRk4zVJUgb5bSM4lDamZZGQeLX/aoS+XnRjAOetCu5Abjx4dxtNVEVjC28Tmwy6x5WgoPJmnNz/sBmE5/wl19dAceZOYKwah7XdO1kZTbvvkfFB08EWtMexh33IFYwolSGEtG4j7r25VJxWFtYXF2AgLY6WEVKOVvsgAWCTui9ecPreOHruKWdm2k5aCxl/X3ZwYylAEE7jPI1mLQq6AoH7a3OhOWP4WCcPtdye6pXj8GK5uR7JP003+cCkM9eBd+SzlR+r+CvPqIMHHyfiRukQnejkhccCK7cl6+LRAV6hCV77s19G06PSiibNN2tReyhSvYL8fKeN+ueyfAOdkcOuPL7u8yPtb+vHgIO8Rmyx5hmhjDZnHHo83LIvr7JQ5V+PdXRdqBFOZhL9/L5O09iaF/60E/fnzCMCUdiD9L1xPSPDUo29WyFP/HJptvcO7lpt9MskflgjNKnIKht+MGCJxwlnIaHqWoTXl6HqxuJyln9X/IRCJc4+QNA7aFG62PHpR9KXEqoMdFg6n6q+1PXXSZEMoPUVbFo+sUaVmDliSCD6eS4BoD1z5KRA1hJ+WCFXuurAbEGEkzeXk7qHqGyFpi2B0aQDqcpB+7GIvwsXe6My+E7lhtT89PkLuQPlKFl23Pb3P8pPHCwqUZCzfEkQkUWaqhG6ALpw7r7JPX+xBkSYVAmvMe0IjyWTZmHPo1by8Y6aNShcryA7uzTeWDuRa8BfzgoQO6m4Elkcabv+qMKR55BjaIUMK+jDKFNQo5ZilVnBBmz88BPTun9/51/6grHrJ+yYnFou12d5Jpcd78u9FttPzMdfWWhphJ6UPBk7aZ/vgg8X9zKoUt7FIOxWT4nfE8y2F+9g95CTg23MKrzKxwfH3EWZsKfGXMfSKCxdCgyPMN4GsLUP7efQI4js3YnyPA+iSMyju4LN6sE+9iJvq7Z3Lb6royO/xBK4Oyt2HpWJIIhDBHuQXbr+Xp/mqziaPCD9zV3rEIqnmx5QMuUJC6yzMEn30gtFP+MWIuNXzEwRz3XkmYpyzOMF0O5AL7vzrp3ivbXyUsOZkKll0+mfjIeI1GdsPxo6pkGCTh6WvEQyUOVN01R/dmI0EFaJ+KZ8N65OhdzJtKS0JGmx0hGyZpQlsA1QP6nAGW0w1RLMHH+pAzfPCvi4kIku7ygcg+1ECAn70coUNp/4rAliCEgrUMIFmvipQerwnIxqxhegFyUkSjtfdYDdIE/B1aJxw3IpbAKlU5dndk9v3pB5teQNhnXBuk3L5kdglc0qUPt5NiLB8MKjZ8J+riTIPF5pEE0KTDRPgpP+0Il4NsNoypeKZKqiT05IGLz/IgTmcNKTRVCBvbh2oSAz6qXK/xmnividpYHnlhzoyvd57+OElbCuFqTkAbu+fG3fQvyTp+FHaL49ANs3m14A/TjG7GW05eChsBhgiTeRdn3NF/Dh+Jf7H8O8UHyVZrd6nCEf6Y8mNJw8ryQ66KzP+2n9gmRXlMrZnKgCbEGghNwmAC+hP0YSA1N/XXo4HbzfNu/HCbSqDUrIWcehxeG+16kCOdUA9UMFgQxRMaVKJeQzVYBgLq9VnmpfP4fi+0SaE4Hr4zpiDoaipOmJc7ueUDT8/ogYCAMxQAe/j+/WBis4see7PIe562tCSf2lrFPO1yeGy6z5B5/7Qn9cBaqtLkemER9Ur2HD+2fC8AMJlV66gsff01PZPehf8SkNp3vjFu0FwtcIBL9WRnvWDUNtdGmDGyxiXTJvthEny8e6IgyaGDLTlojafERRVcQxFf6qjmzYYpWGFt0xMF1aiPGVqqEy0hQRDUIQnl1tbcy9ndohA6gCW9pSiiMEFR97IfExXrE3o2nJynuMOG76NRQw9y5GKPhzXWmZMdV7p0GNf2IMcMoIof1Syl4Y9cyDKI2G8OVSG5jXVJYH4oDmtepNqgURdLB8Xcomqax373thKEth8/ZxzSVVDUGeEEEoQRl53/jVByjjeQgbza4Mg09ePbhhkf3dpyxq3iIrL1df3EM9K+Wsn9T6KLEZwN00sARPc6Nlq1slLHy3gH+u7WMoeOASJv0It5VW3ETvexwID8WoUekoaJg/FF82AFGVPfiGoV/7MXtmK9nZ+3JOGkEcHYA2nb1AxeNKxu2OWzegNS9sqdPqm+U6qvZ9GwtI4nMZHG7wgjll0fXlqkh/uK6gbipsDX/b4EOfzRNZ1rISsBBYozY7aEqwsXoSImlqd6ICjTubphjzBiKccHZ0CcIrmmpptnnsrbedsvBPqAPoeyQcIgGKzjPIrSAGOQhLxk1KkIwXV2vCJBJ8B3TkeCyVv7QwtE8GKGjqehO9WjLs65ITAHS+wRn6MhVLKK33PfaetZq3VnjAzb42e2ZeSOMeILAbefs88X1VMD4d9UK2WSW2QuVvGSAb39/vgSp+UDUxTxAlqmKt6GTHuzjI6gj3B3tzp/sy+7nnawMmt4PpTWZtSLz1vIMVJ6w+Ua7/qrDrZ1yw5BuvLfzMSEhVNF5Bi+EKMpyM1mZryWY1XGwWZzQVurIu3b6SWiYQAz10tDhqrn1I0Fyjfakr9wt5hAsBDp9GKbsoXGUzWp//HKTdML+ewlQXi4K25VL6mXXhDr7d5FV/LRyOzjCL5LTyw0jNdr/UyOlTI8IgZ2HrQjhcmbvHyt/evk0n41EcNXVQYAO0rfHdLbb0v411o/u3Cun2PJ+WEOWvpvOv8meEcrqMrpM4+UyeGHsAGwnWvFkeW2UUEBNE7mpE2NY4QoCbT4/QW66mImR0x1C0FiDJI9twSyEXDmtXK52nVhV/xYZq3RCCpWOfbb0zYvdMml0alBING/fW6fyywol0XZDdp9TmvoFyRyh19HTQCCF379cmvnBuYk9JsrPzslwUrQDqCYBtt5DQr264J+7qr/HCQjWIeuouZspEL+5pUx3ohzDiPDZNijfbX179H8NHfDaGbLzNnuO4eFLSe8DZEwSa7m0fzMqsNySYiYd3UsTMlf3YDqPud5Y0Mnfphz23x3cPKMX/O3Tl8xdr6KO+KdW4yksh5N7DxOxDMcg2Zx7/6rc4cFsnYJ7a/Zli4IppLCHEXyh+HnQQ1YdO02CghNzOJkJGb7+X8+aAY9aJVRXyne0vI65ZhbgsP8bpKEKGR5XQCNanPg94Lecozz6zwwVuiqDrFIbRUihmnWcEEfDzRZeOU8EzNKgIe2nCi6b0PjJwI1GIujo3kD2wFu1tsz18RJq5KjrxaQ8VLPNgIS+y3c6p6TmievDXGE9q24dvkXMctb8RUa+hYaKsPbVMc0JUloQSGzFc07YOyU5GpKmlV+qI4ifXJtMBklfPtTAxe0wVPbxItVLUyd9YOlvE9Evxt53GCRVcFqmu5CMo29wmkBy3/KFbrDd1XjZ0/O6mJe3nH4+O10aCJ3HNrD0DUSUVkLunGqML3bonm3k/o9qAdVbrTzWBzGbwFdROONYFt31/OMPXzXhcxHsYoK/yb3LzELFou3U6yqvWnifbM+PiCJK2aKTlQYM/Pjwf9joCqU06fPkqKi94MQndCvHKN/2CTOPyLUz0GemMwf8kn2mRUUb239us1z5XuT5KDXL61inz3fY6BgAt1xG+8FZOH5yZ278XesMzVqi0cFWBzWrd6Tgh4x8Hd/623x6Znbd+EY8xTPnwJSwGyqvoJrVmAiXDgZGFcaZhx/T7Xid6/LKTR89OI7AZ3+m0uIxvWMoqxrBkNTYmDms6cSRF339Bvjs6D6jRVlgvzY2icZxqxy65C44H0vZoYC9XSAcirIe258JKK7orMnYLPi8TyM2nrJ0OjOwyRkpF/A4GJEO9KcHQd3TkfTMs9xs1YhEhmA3uU5ab+Tu9QisafyIiNeQJt4iRXZnXDVloZeLmSKzqiNjxr07SEpafHlVGNe7G/X3CwaFyCVo1e3DMBRcNp3TOJ2bCy9s/XUuGtvo1gtrzm7yZf9Mh6zmcRiTccNODvZi4LJfzCk0buIPTuvXsH57cpapLz03hwuOTj7jF8mWKm1OM7Uac5ii9DwpXRrZjfUZALZPp/t5zKEGW+tEPMKTZSYzKXSxBSDBxS3fETEFKGLn5ueSmUFHjd58Ssi9enzadbIc2YG3K3b191V14ECmdBPxfJcte9jteb1fo/xFCX/28F0woRvAW/Oq6+GYaQcsF7dK7y2b+y6vBCyzNJtXg97BwacYIh3D/nZJkIxnYI/ehbAfFDMLVNMvE4qcd2fmhpRw7flsrk4DfNiIEgLFD7tUIDjjgyRLjkZsNgzdfgIUGCtjL+IIqlDKxHbe4Jurr2BK0vK2htQGJtR00GLP+xUpDkx5UrMCpjrCYY19P67OwcW4oE0wOTQ+cxFnChNCq49TdGx5X7UNTMPuBAxDw++/UC1QFXxiXtTo5CAGlR8qTeT8yETIqQ41kY876XgQAYO4H6PYSGG3tjAPmpSBZwTpqqPhOcFWBO+f3LZ7MuBRt/258XiqFae3r9EmwlPhK0bxRteS1Umu7mTpMJEJbyWC3A+sH7O6y1ff1kxM1VHVvkPG4SXdoxjT71LUkTlI0Oz+DlI81GmpTWDUUMUgkWXQgfPfAUefdhKxpBRuZH1pP3OnSs7D+CgwVQRkg3zZtAVJ9TpUxP5q8px121tiZl+3A5Aakd8TvdINxupbgFkH6PgMQ+pUGwXKs3bfkov7AFAtxmfkrlZfFb0dm5ULqEKrfKB1rICfGhlJ7denM4wCdGYT7bUfx3Qq9LKAAJK92m3lgnoHK60oDSvlwoeWysslSjmXEWDPzm0hfcWwc6Xjyux3Afq9fLBHXIZvXRO2sOF9FFpcMTWBxNTlii/HbMqJ3mUMyAdXhCoCj4T6ejusKokuw4U0Knk5pyHRxaX5Q7DgRcyy2yIYrO4Sc5hSFCfzEPUXKmwHjle7icF+fNjDeb3HiWGqbNWRN1kVLrXHvtjyG8VJVKKXCLtaTpi8V9j47EdnKe4JMGnFjvws40M1CK3blVZCs9uZVTVMLmaWmOmxZAbd5iTsM8D11ovfWuM6H2BoX1bUiVeGVBQff22Z6AbOjD7yiK5PIIERsgaSqzuGthO1S573orw0cC6+NXFlhGMEfrHoxDzhmtmYgqd+oAmgl4XyULHwucaTe2r312gZG6HNJzvgCYlUiLNq66MDqyfp0au5S5oEkMXznMRCs7yARk6v/RpWjjex7ei1a3C4K63JcOWRE+z3oDdpn1xGMB9I88VaVfobZotzVxrrJIuZF16IqKiIOAirg94bdXKMvLAFlzYGxx2PpFNkSs0HrC0wpLkS/bCymvngSZuyPWrX/yvz6znpecQ1+pLqnoO5Obkm93P706KNUXW2PBHGl2svL8VBQpexU4vinkdGv6yED5f6L/uKAyMMtroGAbm0JGnp2D6Ort2AmnufRivtVD/ecHZLLVePsLhTMSpx3+iYYtc+UDhS/Zk7RR7xA6cdH6aXx0x3VfR+uy98qlnYz7SN0so/A4xapeWjgmVOpa43cEU2v5T69pHFVO1ppf3NSSmKExIfX+vqqEc9tup1GZ0zzCcFfUzsJu5j+Qg9VBjImgzLtYAbCq1fvLXd7H8sr5GduY4ULVNtLVag0B2jcTTcpPg5FYCk0ZkdL09nPqev02+mdKUdXklTBNXhlVuQvRuxmOkYEdAwtDaB+iypFPj97JNp9D+uqe3rWKZKF8NRFCmO0IIS0/kT3H+RVpyxQWE2Pa2+B5WhX2+7/MUsBex3420kCm3BKyg3/yx1Rmv3BWfUE/xbuHUzwlD5MiHVQUu7ex9ogxxcTjvZWIDQ/bBjp/Cn4Eh63ECpcheYvh/N/07hyi6r6WJmAq7r9i5UgGU+SB4opn5nPzGNvHtoLSIbpvdcjuRv6GCvn+V0m+nFsjdwnuT7HBysugzNgrlcwNE1mNvd4aKEtB+62uAprGnq1Cmlhs4xVG9cOG3JcwvX7uh/sRqB2TJVz0+tXwum1kw3Y49iw7nSJjSpW5qZj8xJpltVh7BmBwAs6NYmjHNoDGCFfmmfZwfZCLWzZGh3HE+tvKfKJND94vqvvuXnG6dueYBU9h5AFk7y+MPFTgTsAwOTrt3sT2TrATIdlfRZj8cO+l65YdIlmYusu7RgJLPOouXiIA/RkTdQfl78/vU7M/TTEqFyq+OAPnq74raioMtyA8wSTU/w5kc+x28ePJZuzz51ovF3HS50cXqK6PBZOBQX2DC8U0QxFkgnkJIKN0n82NvsRN49G1l9yZduf/84xNtvy2Yt1A7fWA01QTWz4IqUhR9XFqBw96U52bDuNGUmG7ddgbxf0Hu+L2KfJuMyn8GD20BjPGAK7ZGyGZagrYFI1rpHPJlfAxVVGbrkMyFSYoS3/49R7TfgOhdgwcpOKphBGAFEHc5xgAFjfAlE44Vn3k0D3/gEAjTMOkGh8RmtGs+kaDaR6H4xFIl4Ki1j+pKAumMIi/2wRCdWifxj+ZYXW7YStgMyMq2uy6NCAXQebL4nfE51rDnuEyEvmjlb8Zst7SuN47bbKdLmcgSfBs8bh0D+B60KIEJVP4a732p/3qDbmtLdleFonuSAt+Yrhni3x1DuBQ4smRSymD0djOUKe7G4vxzX5xDc7l4WQ4Y56BdvSV/SHNXf3U0s4P0Q3JKGrus7izRadAnlvOPP7Itr25mn0M2AdxDqjud/T/tJOzdVgP0NMXlUFn2SX8WtasQkzxn1mRDhxeUjd07A/cfJ6pqpcJhDxK2ZD9PJhuHYjS/52y1yYqKnQMQvIouaNpDPOEuEjqQS1385xNzAUqdzrWURA47FzgtVMEilnp9XTccaS873wI9+J6k26eu2qHR9OphlBnT3hV0UI01TF/riyhyssAlocdh5eAWkvIPojcaV07+fxE/yoPc37qOTNGLwzUvPWYQvQOqP2q5H/s9HQ+SSLAMou/wT2mi4bT4WACq3pBMJ7HR38IsaA9w2n8R7i7fO4DtCixtmZ3OX6madIqr/lkCeLEcw820gXYMG1E/9Thc3Nn9B+PLLb3gQSILc1ooqjqYvK2arFO6kiugtUhr8oRvRU/FzZnle/gYGBKgl2cKHJI05yyCYY90Po5ZEI8RyisoPhreBdkpLYjBztNNxcH9T9hZW5HQOdrKeaZ6jqd+7ReBy+8Dph3/gFiY0VhSiHOu2MaZPCcw+O3vYB1+0b6Y/LA3bd7izLX4YftCKLJr4cKtwPEQ8SCA9QUQtxlyKUdDLEAlcqp+w6VcirmqRUkpMTAghEbWkgaSnoPkz8BqyaP0oaOyx6nj6ZXLT+efiWsK0Xk3KhUoIR4QpTAugaQRtR95ydND4hOpwMcEv05M4YXN4kUcJG1lrNRQJAYrLPWc6E+ZfONoat806Rgd5LQwjei7/uMsBLL0uMHX8FQ8L2iW33ehw2IUqoMi5MQZLVJ+KiOifZiXmJpPf0fJP+yTnBhNfP5I3+6lI9cOo+pwecMizovIAIcj8comIXrnoZHGKsZCdd7zh8W7d4zT2O4s+0l8pNi4XgBwAxp33+xke1ppmH9MoHFLJF94fe4QlH3Ug4DCTV77sDZIpxvGjThSI4ZevY7ZFRgU4j7EAY7UZJz4JAS0PTLlDYcALbWEB9/e58EQbAduYVzGAXdwPaEJ11JGwTIJChrV5CIPVwKj3+2HVmH4g+LA5eQiPckR6mCqzhI5lNKlewDDgFzrdbJ+seZrBFlHC/viLYassGQPhv6YxKY6e1CMiSHpsIHAY4WcGCRaphBGqUCxUBbHzpQpZCla2BDSgG9sWzO48Rq59dYGo/krR6rjiPEF9ivHtJmPP8iFB6ySO/oRSd6SbHqAAzfdMkaXLGbeXdLUEbptI97EdrvPhXbgB23+4aU6XzvC0cGOaTKMD1lToA0uKv4dlgfQ/tYRwLKZweRcvGs6JtWg2tBjwJ6hAc2Qi3yNY2lKEii5YirHokkLwkpIUzUEtsS8rw6ZxmCtLc1PeydCFfhmIkgrrumLHza3Wzl2M9tHcu6NMFNJJ9XXalGPwG+1aMt/1ecdX4sq8dunBtSUWUyVCu78timVYlEa/4+PEb7wtNPfcUwyHL7+dAos6tRW5qMsPKdG0czzeITqy0deXqal3LDUf0KIBQgOrahaGsiYInlYVL41u5ZnjhyY0XCxGU1pxndNVPaxpvs7wxKJEZ3ADBn3FOixLY4ja5UskwcOR13TC2wBfbb1gjss76I2lHWJAQY8LrZgABRqiFSg8wqMAeXCok/FYv+tOBgehUa0Ye94ocvcdx/sIRfEfAt7MXYF47HNBVlbmlKxZ0UO11aZM6o/aM9mttUaOICgEMN++6r4rYktc7vf4q1HDZalouJ36nnbtYfhOVGw7c5aZZdmm+sxC5YepHHethxRW2T2a8QWJYugXns5kTvyJE86yFaQMAMpvMgoQkGTL4OedKiR1c8Dil4FSHjYuKInPQQ5AFJl3Ac83ysnKCzaltsB1WVLqoEd3KMwaEX5Ij9rnlYe7lEqgHjaF8PYreoLPB5zwYgenF9GcM8trXR5ConBzuF3t4OI2JTOyVva2w6e3pEoVtp5S1QNQb5WoLCI8UTprcIWcyB26qDfy06+e1EJQfh2IZS69wchfe0SERQSLD+ZYWZwhJv1KyAuQ/V0yUkbTH0+4PxzJ0NrN8akHh3WRrae9SDtP2Na9rHf5zQulBqCJsdNK4mDDb085ll1GOwz/TCUN1NEIHOuD+zmfqTrST74ZsiVF+5uDZKHcKawjE8ZIgs5u3HtUiarEaSrq2caRd+4kVCqxGQSThiRA5R8No2n/h5MkeSdXuiLRuSx2s4mvNEnme9888lhgWXn0pw6jDrtCVngJ2XWdtTo3wGXYmlZkaeHmo745F/jA5GkedeqGU0gcEcOlMTOVkyBWHPy0ewfkw7YBoPVAGrdhGvhmqlOKQmVujBWU89tBvKZ9R06xsdbIgKBRjhH52v1XwFk2wdEO/H7PCYkjYZJB06uXiWDRnmmzqX4U8nocRdTO5cTjNCCbSWX6wj0h63YwgcIXHx+hZCtOKvfbR320e+PEGbx53CvxMGCw8MflHg4HWEVOWmbuvku6ENJYIPRqIGnVhBOGBkr6VgVwwRX1udf9umRDT93tqOKlFCJFtN8YW6/BnKGpcaNPOaeSskQSBULZAXYFLh6eQSLJzN0ekyXOi6LaML2MYjWS6PNxLWmaYJuhOkB+AFs8rDJv6OmDRLE6NCqbe66XHkhAnwxn+BrWlmpXaNTL8k3pSmz8PSH68FWl8mmWZTvU/aX31HgtRkwImcCp2+/J/PJEJs+g5+XVYglyeT72LsGhkxylIgyOZ7l06jx5FKFbnSg+6M3DmdTPIOp4Fy/Po5BZb96DaKdPY7/L/MKPBvVCVB1wHcw/6o2XQHEtHfyF5Y+tPrOF9tvifh0ByUrmuc0HZPRF8CgW3mL7OvJcowNW6BJFYlalcDRG6Xw8Tm7nKyg68h0JCbbH3En335qxtjTN8uxlu96T2IsJCp7vbH/krFRuWNPIXssPY2AxUbuooT1mwWB9obxZDt6qVAOirftyeABMBtkfCsZhmwCRVVxQ7lZtSYCkOMnbvIeYFnaQzw8wpbbijQm7xLdiQBYhEIvUeoEtSw4M51MNaT170ASzafmIGymO/a+P8bq1wyeLp/U/UASkLrjzSgjX4f6rUg4Nm6VvO1hZ8MTMiPyhLq2DM6pMI1ywE7DRTlCV5/+KkzE1kkrJOjkmdJyoPjBWNViWGUTv9o+1bQwvvJZwE2STZgXOW0HsexO4RshKos+IFby8SMlFjjJNOXedDppBQ+VBKNA4TDB7ZyUUh4pTW29Dqo75uOZnXZyNqUhhafaJEs9c/67Y00ePHMnednvHUvFHsE1Z5N+OgJCL3txuWtt2GvTeEogQa43/xcLRJWEkXXAR+bxYVvOJWlzNakWWvW7XQlBXhM7Lp7cl2xfyCaibk3J2jR0sgzE3yY+9I8+z26EqxKGiFA9Fv7UhuaBTZQVXclKPBDnJdUJYYofNUgL0xKb0sUpUUMW57eIi+r6urGdNg/h81e8/XI/Re4VJCyvH3AL5JH8bdIG15EEC+RuTJu4jvrF4gpXZF/+Uc51lOtqEWjRQVxkfe6v/FZYXHyaXE7pFJ6dQ2UysoFIca2TQ4bZAws6Cv2IJjoJ3rInQr9zHrPrXTT5Ub9Ntu4lwFjGFLQQb4vv6bJ/0WSnRFiP+lHOPTupcmge0E9EBc7I5mg/FNju4bOxF70X0t3pOSBscfdWZRT0KqAA88vFwYg58O0BXvXN69BfDbwH6PHOKTjNAXdDnv4mZoyhuSgJdz9wp22vTURf6CqmQqVG8z7nZyPio2xYz0JPAH57Q104o5X4TLnrFYJFJSlR3WmiUxOgkikqK3H2nWgO/5QHvcMKZPcVDjKT+zKzoWqPSskjr90iJLmpYtqqDGuAD7dB7DxVvomeNyaUNnhtpRCPiq99d4xGv/lYpgNvrSMjHCDKWIqHDV01+iTG7QP/3gZFFbZfdLGzVGWCsNYmq2QreygH7eZz4+6ji1fqHiE4e5mgwJRSjckgpvFo55IgcrlJkrqPtIP3fWdGVLg/3CCXDycqSmuHi6O1kM6yXP4wKpBxBEXWd61eFylq1+WpzSKikNOa7dYLuekEkeET89qMntKNAMS7xVj2RZfEuEjHG5dYEXywTDxgzAdvKIn1CpwtiKCHJqu5UZgDGzaOrO+7LJDUN52egKPZnrA6EHXp8K8s/qm4uGfdCSJlov14ZdodAYudYHsodYTU6P/q9/v798kEgqUM/ranYyAKwKkJyhO6OuvuwErnqgqVyDuxEfKJrACKWvDYwfayrYQx0RBrQFouK4YUel6nc+LElCI0DLgpSeDcmV2waUpUlYZmlMyyK87qtNFNUqCHCfjoo4160oxy1HLc1z8uceAh3uoGi9oVSmgc3+sAe7MNaRr6BdqX7Ik5r/PPUC0GHfsjuyf3kBfCMfaT/jRadLEYZ9NFL5/S1x57YVhYOuUERRYnLGmIuM1ssSkwg2pRDfjd+snj4k+jkXQnnwvn1xl+O6fwkE7KIj1XAMCQQ+ILP4JKkkJk7e4lgcQfusmdjKAz+duW4ON38egvSPFB1ehlvV2WbDjGhnau/gx8ToPaovRKZ5kV8r3JXBoL72g0L+QK1pUv8w51zEbWBUuCe6ZgFrrri+NzpVtnMVAHsqNIOkx2ahGnS1M7czGM5Vc0xw5IEp//RgCexvmKUtFVsX8tWCtsG23OdVRrY1MAsEZlOGTIENpfzNrpGnZ1xkG7XqTHpf+KZhLiHVWZyZSa6BNW5eL4YZaPkjW/SR03yDC3sGP3ZUOFTnKxYDfEsXwhfkGS+MfYjY7OyDxkkshvhMrQeOE5XNO2Efg1/xCoWBLv7uWJuKdyDm7Ef1lCXIDUbRT+n2vSbA5jAPQ52dPl+ScleXGjOpw2bKfda9XuxN7OpPRsxerugKyYIDXysFU+D5r293DG7/NxjdFKupO6HKLdEc3EFsfYop3t0Hrnx59da00EOwl6E0sPFP5qR/tIwz+qI4qGGOpLFMqaQaO84CgpHdq3Ccw/VBMHw+usksbok8XBsiJZHsHQ/WI6xkdrbRPEVVy80/ZvSQcl1LPqkKCy9IpeH+SgWcnhKJHUY1E+2OTuXSFdrKFdxMQ3EX4t2Kp33SZqSNYzmOhKl4L16tXmFp/xPT2n7L96IYmhJTbp0dlJ9r9/L5MQP5k4k3m5+PHZv6PoSy5v5tI1Q3sLcWToN++tvyhQ7e6PC2iw3dOAbBQU3ad5iFF2IJ0VpuCpfQEEj+KTkdaExxny2ckFW12zb+nox1CuXaJyTi9AwU0NRBGPUqccnGrfcM1P2HSABVNnobFp/aHvGJpUJ1D0MxQLEnKf3l6kJPQhPKomvkxGY6MEtui24g/hDV5moQQZlHY4W6q5MZP+fnSDEVN4xMoDrip74MGYmo98dEGEg9N4K8SQe/TYOVCMaeffqg1DK2MArsPdiO96l0u/TVhYeKgTZP0ogcaOthVD2ZzrhfB8hGsKV5qHJE6vQZfEnSiEWlGBNbr+CPVtKT0R46X+WPxBQuCOTKGzQDIzOMfZ6CEEyxxlfqypOdY5HmKggvt1U0RPIyM9RaCCRCcrlUgX2cfDVd5i0UaTwhKu4cwtbxXjIbQUrOzHCx+yle+UfJibdOfZpteqwJ/K6lWHLqZCqJvZqaBOHmVtrznzQI55S43ui9bz0F3T0H2XbONglpcNi57qRR+YD8t8xkBAoQD94YNB4pmuiQBi5c3UDSvql1OpTUoLNV2y4pL1ezmuJTV6Hd7gVuGYTCbB9USLqg39ZpBiGPXtlmLNf3hYceMFLCEsqHXopu3rIP2Ip1cJVYjlzqxgnSb3dgr2l3ON3sbeIgXJ7vbgU4XP+bYP9X/M7Wb2hjNhXp2zqIZvi6QbfZV5cv4CqmHOGJbHWa6mS7S3LXyDTfFTnSXHjbVm0AgBAbTkZ+DsUCDL1osiR4YlvDQGCnjk3Q0n/2/ntMQ7MhYQJ+QR57dDF8piMpO8YzWFXuRVYGNYWone2+W5bmJQoL90/6voWUZyQSGlK46Mc8khaCrImgrCaxUQJBUolckjKB5BC9YQ4zp9tsG9UnyI3o3A8zDhhzORAOYXIwv6zjDPnomUOGa+zRd762RhSJkrWUtV1efm9DxTvggKkjrem9RH7iAmFG2HBpoleJtfj7Loh5iADb4BVm1gJgaLjI3d7bsVtUAUKIAUneRBzicCtPtytEJiPhfZ2SpVEbHVZeR/yyTZAH0TGHlCwW0ZdjqcTgE0GyCgPU2GbeT23VqtIhWXoETmyOHIyCkZXM+yxwzoRRS1W201wqePDYR69BMfK5mV7YckUXKZRDfo6akpm8cR2PKkcTzyfT9gBjIHWb609aIUwzHtRd/SPTxR9egO9B8SvtLTpEoKaKAUJ6kXQbNJTQhVZucfA2DF7pUWzUxvJi5812Ll6WuNYtptC9WTJt/3JWT7IwbDH632z1E5AxOZyRAyT3oBHgdyrtMHxSbDZp/kaZzzK5XTOzw9CmZ2UtGxcw1WWh0nO/Qs5yhTk5VF8qv/F15xXhc6TtsrA0EgisWkSfMnTwiUgjImgSGSXFuVcQt13BpXrgOUELpW4rLqixHxAhFE7Wa35umQkDeZkSuNUfl57k+mDz3pYq6x+3Zpprw6XhsWDJV5ilFUZQNwM3NzGhmKVi6KzLU00tMpCrS+PEFpKkgGWfwswGYJwofJsja1VvC4pyp/tbNlcFaCKt2SHZrV2CSu7VfY3tv+VRLV/Mj7W7afonk1U6Nm9onSD4DMK4/kcLdGzPxrjLfqmjUQYLxnP9oaJiEFNo3NaUaw5QDweP8+GxfK0g/qQwVSoCHz1GYJpZbJq63DxnaVNUVtvyoxkF2X5UAHzV2EsaS+LAC9cpqi129d7Nq/BnxXbttcdz1IEQYA5Fnmja/DayxUJCNe1tXkLejQcwT64XCpRlUDXQObylk5pc+Z7Ry+02KnSaotpJu83m7jU5GaNwZzFZV0hV+QU+SIc9tmziWk1C3ZkrqSLt1gbNsYtXI7ZRFVK4oK2x1PavZQG5jkhbhaGt0qkD5GUVKWqMFGlJCbm4ziY6+m9bQiaBeWQvztOE1eETZcDMhKlWPDDdFQ9lo05AYRFDPQ5u0A6oELGfboIZ1bIfy2+4re64//i2tWUYj2jLpXvXp7q1rHsiec/uNV9VTMQaBsh7LPdKGGFjWYQyrD0lfxg/VtLGMHTiekrBGYr9k6hmjyMDiTiO5B+rpGP3wfhPw2GrtD7S4e5UXcnzVkLMKs5cUwKf5UqWNfKdhowYf8OibGqvVe7xCGxjcTGAu0FMEShDm+GWIiSBiNSeE76caRQ1Yukd7Wpzep8zYfLgucN4QtlhUujIgAvGEotsSrSBuE+OTHlG6hfygSb9YEk2S2Of1+LI7PugTPg3LQH63GO0vMlotJP8tZE4I5d/RVSJGimAomrKQv+40xM27v8ftQS5LGnpLrsxYNQ8iefNiEs+3azY1s3WzwdKfFo4mGYgwlUDzdyLCtaxaujjXN25te0/ZQx9QV3/iFtGitwH1G0FQoc9ivxODLwisOW0oeXUnI0Sh6Whg9nMmSYaMyUcxEhWxCSSEW7hZr52HrQ5FfoZl47sSOi31l4NilaNMrm1m9LYAYNyR+4NK4IPro2wtgqGOIq/TyEdjrtpp0nICNc7MP0J/3gggmfcUcQFvqgMce4CJ2m6lA42tygS8ztLwJwvSwsoFBIkzKXjWn13r02KdBVdjo1LT6kQr2DD2+LYbrEBUEtNRSOnTHHvx3YRtSInIH0Th914Tazz0iXyYazC4tUir/330POlCKG4X7kazm7QjWmyhX8UiLlCGuhodgFHO+SsMmeh5aSD70PdVSoJI0TB7ntIL25zPxqEe7f/Kg4KQMxn9s/9gKiep2HS8tLkDwEFQnBKlp7htzhBuuh3z5JC5vttAJbConq+nsv0bg6CcgkaFWH7oCdpd3XPLOM3eWwMzbH680Ivvs9WxklKF06gFD6RA/wdcjLYX5ZZ9Sgn2p3shlmKg1xWmK9wdPCYKpelN4tRs9ZaUby+JcKQGCrK/IF8dSGnILhMDu9JYfbxVMDhEZ+0tcBlgX/OQXmJUxSL1SQQW895NWxgyyYmJAkDbpNCiEZ3YY2vrCr+ljYi/XbNsywkxlpNUucQzHfgbZITLPx6ngJ2VT1Lg0V43d6rmRrzmcn37voI7qvL0IYdU9zGGiOHNRUGJTVilxxEEsRYlSj6EHVD0zjemG4rhPFrOqBFsKecNwwUmWgVa6TRpGLzYuUSPQ+Z8l8TglQbnAtWOB3BEEYf9ZZswv5CXlloHAh9eFrFob7DpJoIaC0hSPZlQpljpnibj1mPwg+nWJBR4ea9uz+kiwJIFw4gYUX1Uv65w09G3lwXehrw/xc5bhWapzhIq2Q3CkUFVrk2GefbBAj8Dv1zVRLfFLyQwFD5ZrvOXj2WCunWrFcQWqkDu0I3FYn87g++eQQmptDcRl2Ul1WH1B3wgihgyG8I7VnxfE/Y9ujSeTlJSDrA29F+t7RudkND8Mnu+ZezOyH0PMh2vGLRvSTn8mkrO4PIyqADknKkNp9KZsmKNkxHgnChx6BSD+0xySwg6TVDKt7A6gcgyCEyO0tdWtcjo3SGt0yydirVNUhjnWOCRGpcMI87JRUXEE0xWsswdBx7DhDqGGERhZj0kobqwxzRRp3GZRV6SkavnaWVHlOYZKA9ppq3pgOVZDECg9Ryp/xW1G8HwgXD5IN23B1wsUXZ3EzGj/puMnBBdK2ODBMtl6UoS5lLENNMk2YozYzETYmMCMTbg3KUpuQlTR7XhSrmXoy2b78yF6tW6ial/dSB6H1C1DVzqm5/A+M/bxokv/1DkanSzl5xGrmUoyZ6PpWmgQm2XEmNOzxVlEBDarMG6RSPI1IW0CS3068qMs3wOyJ1k4sbhzq87xP4TisUQoI9AdiogT6bFOaNzFCOkEqZhGWo0gTp2EUstIUu2M/Dwpfnjk7KwdK19Pi8lree1e5Dm5hS192FSAtDWRmBYcYgAJSDs7qwmW0wk0xpppIJ1VKPgVSFG0eVRY2GLZE+MLxrSa5qwp0jgGQXwNRmBW+TorZrFk3/EY5lUhyeNV/8tEIVpjKbFCumKqfXVgBqtSwcI4+P+BYLhZapgG1g8ET88OQoQ4+w2ZSqNcM/hnOhHpcHeSaPNr3whK+ZQ2XXOgHjLd4fEsN6v30JjuS1dcx1Uz0I+OujMyeDbwqzxx+4HakWI8Zub9Q4U0Wvxh8yjDhfCwcHFi2Fg1ee1+iD/fyt3zJeTSgGIiWW76v/s0iFnMtYSrIlw969F6zdOJyPpOkCkIPGetLdviNKb2xTiZ7cONoeC+AmE9vy/yF1NL3QIex+3d9PSpjgqXLY+0sz+JkldoRwftsZBA/Bl8ih/NHYKzDQjm1Ejp2NQNUAySjYLP63Uuhp5q/4offZREPrzHCjXIP/FV+T7cL7gQgjBqFWxBLwBAEx/J8WxWXzcOVLe5RFW3cwS0a95TY8hqUJLoKLwv5ezIqEz5SxlweHGuPkafjCq095e9M6Zsxk7JXXD/iXKAzSRpRXkA93PGeNALNqihPSxjU9PfU3prLvw9aQGkJ713m1z34yievTHxB/x3An8TOjNHWUSQkebzFHCnoUtS/6p5j4uhyEamGo6bDj/SUjdpFhfsOhJaBx3Av3XSDPQX4rjoF9pKrVXCVASvx4KBxgZsN+l2q3t9XSdfzyjyaF5be39xr7wZFuwT13PNA7K29FjSc4Ag4dtAdoKDZwKQboC2RD5XzALsUVKL3nvByvdpCfNsAPR0dx8Y9YTh07/2Ig7n/7MlEUcoC+zJhC+4CFeZnh2b2cNbG56YiG1vXTyfrWXSb3qYe8m4fv6C14dTSeg+gIrd30UxZIcrKF+Vlmd1vPcpGRdzfhX/yW+VejM8ak8bsA+n0tX6WAU7OKAk0LouYcfyyCAzqFmhrBcWjmew+uiECDbK3h5vc/VT4K9PhWx5pqWJ2pDHJVnVUBq+8o4NvoH9HRI3qNakqJ+gczfHxtFvrQw/H9SQBt6k3FQvlHZaWrnbz+RnaGj/JB8KpeAbrzNZ0a59aVDrNAXr4CZ9c7cduKQxnaci6VwvcWUxgjeQD6GX487JlBxakgzpe/17T/Jxm1hbKms6yZtd5JtXxsI3vDpk5KkPDUH/x8NTQNnqcHuGwtsDlwMKGm0CgFrPII8B866Z7JXhICr7GVhuvwReSgyU50+RPMIO3aWYo35mTX1I2uQ2r7NrtIka1uHSsDgDDJrSKO1srwSbj57AapiwDYLsawqjVLt5vAD/lfbrGmse0zSSpV5bYg8vxEfrxg1UL61oUvUEbVWNP+Yxlsqd4ykMYHwhDXX95j2aKCgGuwgDHbSczQEyXmwmQJo7yWHPYQBTIRl4bvB7SnKHWmdS/8wGKehIcnMuIP/B2tEjS0ELdwzVq4MebKO8eOQV0BzuIme+rENysV+jMIYZu4LMbnv8oyYvtSKKAc+yswp5SWajvG+nfHW9hb4+nQhgPokR56eC6qSKvTFy+F0WYJ9vgaD8G2DvtDrQDBfHA9w7Tvdmhc/MyvmAwl4vfofTuiJA4arZH+nranAaQGKe4US8c2c6kBf0a3i0ZucT5/oSUKGD8+QE1VwT8hRUVhEgSN5xN7rJvzu0MuwhoTyR7C4+R3pzQXRfHXJ8/v2W3ECsYjwTH0/FRCcLkMgbTCCmL15E13h2DSWq5vXA2M5NpIhm4zBSfucPAkFnk4eN6pkxPWOlaGTPuOQRDqRmiTGDrJcwEvMFYGE2VwV8d3RG6Smi4Pi5sKLc0iIclCUyDrMBhZAUib8ZASdkLoHJf2z0d7vE3DniCb1VOOIHtKhumHFGEtcRZiXc3i7cUlxETP7Pijcz7fwQqkiRkjJBwTm6aLrCyrD/3lhEoTfHSprzLC3LgcbEiqAfeTJp6VBZQ5O+/8lWf/TKats4UOi2sNUegMJVQs5KNyGxS73XQ5jgEcL8YZSZ4zUfkle17vdNXcJ9tS8X9KhXffkuYSJssf/EMq323BJFUvcKDEDKw5CLv7KqJ4T75KVls6Oq37b3FMZoumlsfQLSELi20qsOLyj1X3FbmZK2dvV5YhwEALMl7Eajr7kCQmJctk17Ckfa6dga8BQeFQpxA2+TTYbGA1ineEEOi/hPFdYST43Vg/GSyJPF5RErmPqXfKKGfV0noKhUAKAAeYvcSfija2Oy6cc17bQlhx0pgEhlhQVejvq530sdFIYKraSCWanqbGad6yOw7TdZHIrffUIQMEk4FaMXpYYwpKu4d0Ng5BpiCUNgmKj5Gp84RfsNDy0qh+UiHS1jybqm3EW03KF7z/T0FY9XrjgOV32lDMGBmLcHJ0VrboVHniEMEkwAEkpuIYnIJ4pmnC1OpVi959YHpPBCHuBxx7I3EGv1nbYI22rY40jUx3VHb7b9+Kt8fd/JRyp0ISqUUi8Dpmw6FnAPei4TfF0TH+XxALtSjHnD4/l/1gUMaS94TGcQvH9zixSBtqmiWJg7+szCosmnx6YnJZt38vkXXMc5z9sEnrFfeaMBPExPAegjbIDktjgKx6yldZZnu4wVVkxpyrzqeh8UC0isS4rfQ5t3Ogbe2qNXjMfmcA5NjH839EP6Es4HQ1KNlBWxPhlD7bbWO7vZ5OQBzxI9yX3Z4Lj5KXm9qT94pY9JRA9wQcFTf2NVnIiW279WfoSWKXIf5qY2xRZbxcN76RjLKdE7bPXzuMjHVAatLfzEINtaM768uDE/7DlCmR/ayB3FKkfVHnQ6raoroSyEsaRFKOdBIDkfAY4pqeTB+QaVk+n58E6XbveVa9JnHkJ/Jfpb590tnIUOXK/eNAoJw3eGyyAneUD6d0/bcKrylMHZVjAaMsSuXRfmiemCbHKUit7+0Td0HigfIgz4upoWf3KI8JYoW50//le49dX7IJXsWaMCVI6U/YFDyA9q6DLfjyTY+aEX3aYPTfFkolyQM2Btk1m9c3RWs0htd3jqbXVL7N+oAsb6bM6V54SkKgjPwKOlLQ2wUqy39Wpgj4fW5So/fscE/SpHp3iFYz929Q7nYu8ObjRlV/NT4CG9a7Q6X9KCY/0K3lxEMh4DC6ta5E0uA8tZ0/2lrJ4yyE3k7f4W0TteSvKE6+4tmohQtGvjrOdZuQB5Zib9aBbTXOrzISA1Vo5OGhH/VP10D8mT/DoyuYgjARRdRfgqTEHqCAHdL3z3Agqr2t5LAjd5sKd5eM6h27sXKW4D7Mgpcf3L+aMLk6Cp/ONyPpBvisCiSxwTuhU3Xv6Fv0UOqP3SFX9XPis79y8wjCdEuTcVAJzunEik5Bct2hj08Pu7Iaf+3kROUnL0TK9FKfEQ9ztW5Ueza506UWiSmboavxSIomsQd5F2s6xha/KUA2+SjxBzVXHJmXoOsTEaa4K4oYXcQ7Dz4vdvWl5g5UMFHTcjAujWmaurCbkyWvq6H7QcuXHch/oGe6yOb8b/hWeJ9ECKPdR2e4TJWV2yxhi94mRZmib957mHkNg0YJo2HPK4X2701yPF/tCey3Gx+wwEn0j8Te2EjNK7+5qgQD2rQ0QP6CXAtAOo8yZRdnUoQpOgfOpkP+u+8fHGP/CcjFcsIHHvPWXkPloyX8X27+xY7LcmDn+PjUSskB4NcevJtb3b4HJA7VQboKT8qF08Ib89sSh6ViOqtnArBmunIq1NgsWX3sm7iJsgt3CileSu2S+77ZU+2UNurKuKG60lBFFDAyeeRp34ZMZO3FYNIOVwk4Vdai9y1dGDFu9fyGPX149iVtqxEIyavJAKhb/NVZqb/ucEPdAZphJ8aUgeO624qpzwJeIdKKaO1n+Uh/0CtSXjsGu+2qh/SrDxZro1loz+TuEoMRjX7MGd3hRI6ap5xH+XbwON+qs8ZFBPAkBIwjNy0IJKaI8/j47SivWGwnb4VNU7lfQ0G9LdwwnjG+vUVRi+3KTPmLrDxqFcUme77fmLTWB1umKikLXiax6dURnlk+4O9yTG8wA+5tzvqDum+dwCllYxhPRbRh8c1ld5iW/PcrCL3s5vQZhlgzfFx920taheg6T1S1jHeXEIHH8KJXFnY+DeC+voUL2tPtHAcEUOu7qGfD8tNIp4kdCazrWIhwLLYTxkUhMRkObekAX1eedXBFw+VMvE4Kh6q9XlYLxrgmLob2/I0ipyoU9Tbx3rFONGg2fwSoh4o5j9ZKKCZSrmjEAYO8cCStIY4BK8K46lOYdEbP7IrutYJJ9ZLo8xarRw/+EwvMMdtmMtKdY8GW2dWcDdayMSzxmfLS+dlKEXqHHgx0KX1mjGSFuljYXKY0UE8Eks+wCmzdREcm43WDS1z2FoiK9zv+OemcsphLMKKatnSk89B/iBVw6jw5duHmgcCAkRrYdZ7sciNkLmzBQuHRb0mrs8dFmAMAfRt4JwjfJug/yWZFPIvmdrwnnUFIHYh02h0YD8ScyVSf8LUUKT/FCb5rNeFcQvi3TgkCAe9pR+pNmIV9mQSXPQ/4W7TUEvcbZhMXApw49lfOMex+cGNE526daTMAB3BLtR65zzycB+l5oWZ9/71RegbYKDic+WWLIhMBg+AYhLrBhmp+nxL/Qp9JqeMFOxUvnBONWebME6fFetDbTkxwqOHW6a/GmlcWfr2TKvynHL5z0eUe/RntOyIhG8yyTK13kLQ3ZYQA+6wVzP35DjapqusZ5zRTJmdHByhhq92XUP5HTfbC2VcDvY4bsMUku5pePQPXqd3GWvh6Sh2RFubb8Qwfe5TbkAXihwq1d6PwVpsl5JTeQcIdu8jxd0FRo30p/1xgKva3Y2G45vUcnaChCFN2XSz9o4xymIpwKADS456mYcrFMMwRl9E21XjdksgTssxDECk82oDzXBHbTfP5w2KSPCURXu+LKtGQfEF+PoRaNFeluLYoXYTyzqX6XvrMuFAndiJnIm6kgIWsQa49PeBRnD20Iiv+g5RMJCw6kRwIjcvvX8edebysthU8mRA+CEY0X5QdJ3U3OGka3NL8WHUuZqM+RHVpxYWJEoGAE="}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":2850,"cache_read_input_tokens":60531,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2850},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYen2YGgSsKNmG7cCJLhp","type":"assistant","uuid":"28eec625-58c6-4cf2-a723-9538450a6ff2","timestamp":"2026-03-02T19:09:01.026Z"} +{"parentUuid":"28eec625-58c6-4cf2-a723-9538450a6ff2","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01Cdbyk74pB6wG2GfhYSor94","type":"message","role":"assistant","content":[{"type":"text","text":"Now I have the full picture. Let me implement this."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":2850,"cache_read_input_tokens":60531,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2850},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYen2YGgSsKNmG7cCJLhp","type":"assistant","uuid":"4030775a-0ff0-4929-8e11-1b4c3ca9cc82","timestamp":"2026-03-02T19:09:01.195Z"} +{"parentUuid":"4030775a-0ff0-4929-8e11-1b4c3ca9cc82","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01Cdbyk74pB6wG2GfhYSor94","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_016WfdbJG13y2ybdPhSXHGT1","name":"TaskCreate","input":{"subject":"Add `File` field to combined StoreConfig and update buildEmbeddedIdPConfig","description":"Add `File string` field to `StoreConfig` in combined/cmd/config.go. Update `buildEmbeddedIdPConfig` to use `authStore.File` when set instead of the default `path.Join(mgmt.DataDir, \"idp.db\")`.","activeForm":"Updating combined config structures"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":2850,"cache_read_input_tokens":60531,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":2850},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYen2YGgSsKNmG7cCJLhp","type":"assistant","uuid":"dc165d51-1d37-42ad-810f-2ca9eb5eeeff","timestamp":"2026-03-02T19:09:03.035Z"} +{"parentUuid":"dc165d51-1d37-42ad-810f-2ca9eb5eeeff","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_016WfdbJG13y2ybdPhSXHGT1","type":"tool_result","content":"Task #1 created successfully: Add `File` field to combined StoreConfig and update buildEmbeddedIdPConfig"}]},"uuid":"fb4ca61f-2d0c-4a6e-8535-b29f5cea98fb","timestamp":"2026-03-02T19:09:03.078Z","toolUseResult":{"task":{"id":"1","subject":"Add `File` field to combined StoreConfig and update buildEmbeddedIdPConfig"}},"sourceToolAssistantUUID":"dc165d51-1d37-42ad-810f-2ca9eb5eeeff"} +{"parentUuid":"fb4ca61f-2d0c-4a6e-8535-b29f5cea98fb","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_013K6DXGY5MrHtdac86kxgsT","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Bz2d8gGJ7xgrFhBzG9a2fo","name":"TaskCreate","input":{"subject":"Add sqlite file env var support to main store and activity store","description":"Add `NB_STORE_ENGINE_SQLITE_FILE` env var support to `NewSqliteStore` in management/server/store/sql_store.go. Add `NB_ACTIVITY_EVENT_SQLITE_FILE` env var support to `initDatabase` in management/server/activity/store/sql_store.go.","activeForm":"Adding env var support to store packages"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":8840,"cache_read_input_tokens":63381,"output_tokens":179,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":8840,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYenDHS1h128SwHqN9KV2","type":"assistant","uuid":"d9642047-574a-44c7-96a8-3b1409c7a617","timestamp":"2026-03-02T19:09:08.195Z"} +{"parentUuid":"d9642047-574a-44c7-96a8-3b1409c7a617","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Bz2d8gGJ7xgrFhBzG9a2fo","type":"tool_result","content":"Task #2 created successfully: Add sqlite file env var support to main store and activity store"}]},"uuid":"28ed597e-6f82-4e22-aa7e-040a0d9029d8","timestamp":"2026-03-02T19:09:08.225Z","toolUseResult":{"task":{"id":"2","subject":"Add sqlite file env var support to main store and activity store"}},"sourceToolAssistantUUID":"d9642047-574a-44c7-96a8-3b1409c7a617"} +{"parentUuid":"28ed597e-6f82-4e22-aa7e-040a0d9029d8","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01MBy5YJw1RyUGPjmwJq85vE","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01BdaSiGzRxbajYdw7erVQvg","name":"TaskCreate","input":{"subject":"Set sqlite file env vars in combined initializeConfig and token.go","description":"In combined/cmd/root.go `initializeConfig()`, set `NB_STORE_ENGINE_SQLITE_FILE` and `NB_ACTIVITY_EVENT_SQLITE_FILE` env vars when store/activityStore file fields are configured. Also update combined/cmd/token.go to set the store file env var.","activeForm":"Setting env vars in combined server startup"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":210,"cache_read_input_tokens":72221,"output_tokens":178,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":210,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYenDbVTQCT11eFSUZ3JN","type":"assistant","uuid":"89331cc2-1e8a-43de-8658-7f228d1f04bd","timestamp":"2026-03-02T19:09:14.407Z"} +{"parentUuid":"89331cc2-1e8a-43de-8658-7f228d1f04bd","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01BdaSiGzRxbajYdw7erVQvg","type":"tool_result","content":"Task #3 created successfully: Set sqlite file env vars in combined initializeConfig and token.go"}]},"uuid":"e6731941-8907-4c37-9f75-b9c25f9c59f7","timestamp":"2026-03-02T19:09:14.430Z","toolUseResult":{"task":{"id":"3","subject":"Set sqlite file env vars in combined initializeConfig and token.go"}},"sourceToolAssistantUUID":"89331cc2-1e8a-43de-8658-7f228d1f04bd"} +{"parentUuid":"e6731941-8907-4c37-9f75-b9c25f9c59f7","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01KHrkXMnuP1rTJgVSrsrUFd","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01P7zLWqLHhr4McX87dLJZbE","name":"TaskCreate","input":{"subject":"Update config.yaml.example with file field documentation","description":"Add `file` field documentation to store, activityStore, and authStore sections in combined/config.yaml.example.","activeForm":"Updating example config"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":211,"cache_read_input_tokens":72431,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":211},"output_tokens":19,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYenE5VSjJbCPrWwLDQmQ","type":"assistant","uuid":"b4710915-f841-4202-8029-1e5ead101228","timestamp":"2026-03-02T19:09:23.215Z"} +{"parentUuid":"b4710915-f841-4202-8029-1e5ead101228","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01P7zLWqLHhr4McX87dLJZbE","type":"tool_result","content":"Task #4 created successfully: Update config.yaml.example with file field documentation"}]},"uuid":"e55779e8-d93c-45f8-b95c-441ed8909b7f","timestamp":"2026-03-02T19:09:23.262Z","toolUseResult":{"task":{"id":"4","subject":"Update config.yaml.example with file field documentation"}},"sourceToolAssistantUUID":"b4710915-f841-4202-8029-1e5ead101228"} +{"parentUuid":"e55779e8-d93c-45f8-b95c-441ed8909b7f","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_019UCp9euPpv2Trz2tUdevJZ","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Ry72BVd4oUTda3ivYL4W5Z","name":"TaskUpdate","input":{"taskId":"1","status":"in_progress"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":152,"cache_read_input_tokens":72642,"output_tokens":72,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":152,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYenEjyy5hXwx792uLd9r","type":"assistant","uuid":"c55d65ac-665a-4bc4-af4f-c9f38b4d3716","timestamp":"2026-03-02T19:09:27.410Z"} +{"parentUuid":"c55d65ac-665a-4bc4-af4f-c9f38b4d3716","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Ry72BVd4oUTda3ivYL4W5Z","type":"tool_result","content":"Updated task #1 status"}]},"uuid":"c1dff835-72e3-4f11-aa0c-b5b5505436b6","timestamp":"2026-03-02T19:09:27.445Z","toolUseResult":{"success":true,"taskId":"1","updatedFields":["status"],"statusChange":{"from":"pending","to":"in_progress"}},"sourceToolAssistantUUID":"c55d65ac-665a-4bc4-af4f-c9f38b4d3716"} +{"parentUuid":"c1dff835-72e3-4f11-aa0c-b5b5505436b6","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01C9hWR5wMLanmvhoxrtgWez","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_011NCRSVCofFaBDEJ4dz2PB2","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go","old_string":"// StoreConfig contains database settings\ntype StoreConfig struct {\n\tEngine string `yaml:\"engine\"`\n\tEncryptionKey string `yaml:\"encryptionKey\"`\n\tDSN string `yaml:\"dsn\"` // Connection string for postgres or mysql engines\n}","new_string":"// StoreConfig contains database settings\ntype StoreConfig struct {\n\tEngine string `yaml:\"engine\"`\n\tEncryptionKey string `yaml:\"encryptionKey\"`\n\tDSN string `yaml:\"dsn\"` // Connection string for postgres or mysql engines\n\tFile string `yaml:\"file\"` // SQLite database file path (optional, defaults to dataDir)\n}"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":90,"cache_read_input_tokens":72794,"output_tokens":263,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":90,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYenF1WcBGh4Lk1ckmwZr","type":"assistant","uuid":"a349ca91-b995-4a12-96e6-2d7cd7c9b8b0","timestamp":"2026-03-02T19:09:33.406Z"} +{"type":"file-history-snapshot","messageId":"a349ca91-b995-4a12-96e6-2d7cd7c9b8b0","snapshot":{"messageId":"b8b5d4ff-341a-4f5e-8676-d4c15bf82eae","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v1","version":1,"backupTime":"2026-03-02T19:09:52.596Z"}},"timestamp":"2026-03-02T19:04:01.953Z"},"isSnapshotUpdate":true} +{"parentUuid":"a349ca91-b995-4a12-96e6-2d7cd7c9b8b0","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_011NCRSVCofFaBDEJ4dz2PB2","toolUseID":"toolu_011NCRSVCofFaBDEJ4dz2PB2","timestamp":"2026-03-02T19:09:52.604Z","uuid":"1853f4e2-a391-49ca-a041-197887621c48"} +{"parentUuid":"a349ca91-b995-4a12-96e6-2d7cd7c9b8b0","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_011NCRSVCofFaBDEJ4dz2PB2","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go has been updated successfully."}]},"uuid":"38b3c5e1-bfac-4fa9-a813-52fcc6575800","timestamp":"2026-03-02T19:09:52.602Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go","oldString":"package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/netbirdio/netbird/management/server/idp\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n\n\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n)\n\n// CombinedConfig is the root configuration for the combined server.\n// The combined server is primarily a Management server with optional embedded\n// Signal, Relay, and STUN services.\n//\n// Architecture:\n// - Management: Always runs locally (this IS the management server)\n// - Signal: Runs locally by default; disabled if server.signalUri is set\n// - Relay: Runs locally by default; disabled if server.relays is set\n// - STUN: Runs locally on port 3478 by default; disabled if server.stuns is set\n//\n// All user-facing settings are under \"server\". The relay/signal/management\n// fields are internal and populated automatically from server settings.\ntype CombinedConfig struct {\n\tServer ServerConfig `yaml:\"server\"`\n\n\t// Internal configs - populated from Server settings, not user-configurable\n\tRelay RelayConfig `yaml:\"-\"`\n\tSignal SignalConfig `yaml:\"-\"`\n\tManagement ManagementConfig `yaml:\"-\"`\n}\n\n// ServerConfig contains server-wide settings\n// In simplified mode, this contains all configuration\ntype ServerConfig struct {\n\tListenAddress string `yaml:\"listenAddress\"`\n\tMetricsPort int `yaml:\"metricsPort\"`\n\tHealthcheckAddress string `yaml:\"healthcheckAddress\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tLogFile string `yaml:\"logFile\"`\n\tTLS TLSConfig `yaml:\"tls\"`\n\n\t// Simplified config fields (used when relay/signal/management sections are omitted)\n\tExposedAddress string `yaml:\"exposedAddress\"` // Public address with protocol (e.g., \"https://example.com:443\")\n\tStunPorts []int `yaml:\"stunPorts\"` // STUN ports (empty to disable local STUN)\n\tAuthSecret string `yaml:\"authSecret\"` // Shared secret for relay authentication\n\tDataDir string `yaml:\"dataDir\"` // Data directory for all services\n\n\t// External service overrides (simplified mode)\n\t// When these are set, the corresponding local service is NOT started\n\t// and these values are used for client configuration instead\n\tStuns []HostConfig `yaml:\"stuns\"` // External STUN servers (disables local STUN)\n\tRelays RelaysConfig `yaml:\"relays\"` // External relay servers (disables local relay)\n\tSignalURI string `yaml:\"signalUri\"` // External signal server (disables local signal)\n\n\t// Management settings (simplified mode)\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tActivityStore StoreConfig `yaml:\"activityStore\"`\n\tAuthStore StoreConfig `yaml:\"authStore\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// TLSConfig contains TLS/HTTPS settings\ntype TLSConfig struct {\n\tCertFile string `yaml:\"certFile\"`\n\tKeyFile string `yaml:\"keyFile\"`\n\tLetsEncrypt LetsEncryptConfig `yaml:\"letsencrypt\"`\n}\n\n// LetsEncryptConfig contains Let's Encrypt settings\ntype LetsEncryptConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDomains []string `yaml:\"domains\"`\n\tEmail string `yaml:\"email\"`\n\tAWSRoute53 bool `yaml:\"awsRoute53\"`\n}\n\n// RelayConfig contains relay service settings\ntype RelayConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tExposedAddress string `yaml:\"exposedAddress\"`\n\tAuthSecret string `yaml:\"authSecret\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tStun StunConfig `yaml:\"stun\"`\n}\n\n// StunConfig contains embedded STUN service settings\ntype StunConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tPorts []int `yaml:\"ports\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// SignalConfig contains signal service settings\ntype SignalConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// ManagementConfig contains management service settings\ntype ManagementConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDnsDomain string `yaml:\"dnsDomain\"`\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tDisableDefaultPolicy bool `yaml:\"disableDefaultPolicy\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStuns []HostConfig `yaml:\"stuns\"`\n\tRelays RelaysConfig `yaml:\"relays\"`\n\tSignalURI string `yaml:\"signalUri\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// AuthConfig contains authentication/identity provider settings\ntype AuthConfig struct {\n\tIssuer string `yaml:\"issuer\"`\n\tLocalAuthDisabled bool `yaml:\"localAuthDisabled\"`\n\tSignKeyRefreshEnabled bool `yaml:\"signKeyRefreshEnabled\"`\n\tStorage AuthStorageConfig `yaml:\"storage\"`\n\tDashboardRedirectURIs []string `yaml:\"dashboardRedirectURIs\"`\n\tCLIRedirectURIs []string `yaml:\"cliRedirectURIs\"`\n\tOwner *AuthOwnerConfig `yaml:\"owner,omitempty\"`\n}\n\n// AuthStorageConfig contains auth storage settings\ntype AuthStorageConfig struct {\n\tType string `yaml:\"type\"`\n\tFile string `yaml:\"file\"`\n}\n\n// AuthOwnerConfig contains initial admin user settings\ntype AuthOwnerConfig struct {\n\tEmail string `yaml:\"email\"`\n\tPassword string `yaml:\"password\"`\n}\n\n// HostConfig represents a STUN/TURN/Signal host\ntype HostConfig struct {\n\tURI string `yaml:\"uri\"`\n\tProto string `yaml:\"proto,omitempty\"` // udp, dtls, tcp, http, https - defaults based on URI scheme\n\tUsername string `yaml:\"username,omitempty\"`\n\tPassword string `yaml:\"password,omitempty\"`\n}\n\n// RelaysConfig contains external relay server settings for clients\ntype RelaysConfig struct {\n\tAddresses []string `yaml:\"addresses\"`\n\tCredentialsTTL string `yaml:\"credentialsTTL\"`\n\tSecret string `yaml:\"secret\"`\n}\n\n// StoreConfig contains database settings\ntype StoreConfig struct {\n\tEngine string `yaml:\"engine\"`\n\tEncryptionKey string `yaml:\"encryptionKey\"`\n\tDSN string `yaml:\"dsn\"` // Connection string for postgres or mysql engines\n}\n\n// ReverseProxyConfig contains reverse proxy settings\ntype ReverseProxyConfig struct {\n\tTrustedHTTPProxies []string `yaml:\"trustedHTTPProxies\"`\n\tTrustedHTTPProxiesCount uint `yaml:\"trustedHTTPProxiesCount\"`\n\tTrustedPeers []string `yaml:\"trustedPeers\"`\n}\n\n// DefaultConfig returns a CombinedConfig with default values\nfunc DefaultConfig() *CombinedConfig {\n\treturn &CombinedConfig{\n\t\tServer: ServerConfig{\n\t\t\tListenAddress: \":443\",\n\t\t\tMetricsPort: 9090,\n\t\t\tHealthcheckAddress: \":9000\",\n\t\t\tLogLevel: \"info\",\n\t\t\tLogFile: \"console\",\n\t\t\tStunPorts: []int{3478},\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t\tRelay: RelayConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\tStun: StunConfig{\n\t\t\t\tEnabled: false,\n\t\t\t\tPorts: []int{3478},\n\t\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\t},\n\t\t},\n\t\tSignal: SignalConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t},\n\t\tManagement: ManagementConfig{\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRelays: RelaysConfig{\n\t\t\t\tCredentialsTTL: \"12h\",\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t}\n}\n\n// hasRequiredSettings returns true if the configuration has the required server settings\nfunc (c *CombinedConfig) hasRequiredSettings() bool {\n\treturn c.Server.ExposedAddress != \"\"\n}\n\n// parseExposedAddress extracts protocol, host, and host:port from the exposed address\n// Input format: \"https://example.com:443\" or \"http://example.com:8080\" or \"example.com:443\"\n// Returns: protocol (\"https\" or \"http\"), hostname only, and host:port\nfunc parseExposedAddress(exposedAddress string) (protocol, hostname, hostPort string) {\n\t// Default to https if no protocol specified\n\tprotocol = \"https\"\n\thostPort = exposedAddress\n\n\t// Check for protocol prefix\n\tif strings.HasPrefix(exposedAddress, \"https://\") {\n\t\tprotocol = \"https\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"https://\")\n\t} else if strings.HasPrefix(exposedAddress, \"http://\") {\n\t\tprotocol = \"http\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"http://\")\n\t}\n\n\t// Extract hostname (without port)\n\thostname = hostPort\n\tif host, _, err := net.SplitHostPort(hostPort); err == nil {\n\t\thostname = host\n\t}\n\n\treturn protocol, hostname, hostPort\n}\n\n// ApplySimplifiedDefaults populates internal relay/signal/management configs from server settings.\n// Management is always enabled. Signal, Relay, and STUN are enabled unless external\n// overrides are configured (server.signalUri, server.relays, server.stuns).\nfunc (c *CombinedConfig) ApplySimplifiedDefaults() {\n\tif !c.hasRequiredSettings() {\n\t\treturn\n\t}\n\n\t// Parse exposed address to extract protocol and hostname\n\texposedProto, exposedHost, exposedHostPort := parseExposedAddress(c.Server.ExposedAddress)\n\n\t// Check for external service overrides\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\thasExternalSignal := c.Server.SignalURI != \"\"\n\thasExternalStuns := len(c.Server.Stuns) > 0\n\n\t// Default stunPorts to [3478] if not specified and no external STUN\n\tif len(c.Server.StunPorts) == 0 && !hasExternalStuns {\n\t\tc.Server.StunPorts = []int{3478}\n\t}\n\n\tc.applyRelayDefaults(exposedProto, exposedHostPort, hasExternalRelay, hasExternalStuns)\n\tc.applySignalDefaults(hasExternalSignal)\n\tc.applyManagementDefaults(exposedHost)\n\n\t// Auto-configure client settings (stuns, relays, signalUri)\n\tc.autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort, hasExternalStuns, hasExternalRelay, hasExternalSignal)\n}\n\n// applyRelayDefaults configures the relay service if no external relay is configured.\nfunc (c *CombinedConfig) applyRelayDefaults(exposedProto, exposedHostPort string, hasExternalRelay, hasExternalStuns bool) {\n\tif hasExternalRelay {\n\t\treturn\n\t}\n\n\tc.Relay.Enabled = true\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\tc.Relay.ExposedAddress = fmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort)\n\tc.Relay.AuthSecret = c.Server.AuthSecret\n\tif c.Relay.LogLevel == \"\" {\n\t\tc.Relay.LogLevel = c.Server.LogLevel\n\t}\n\n\t// Enable local STUN only if no external STUN servers and stunPorts are configured\n\tif !hasExternalStuns && len(c.Server.StunPorts) > 0 {\n\t\tc.Relay.Stun.Enabled = true\n\t\tc.Relay.Stun.Ports = c.Server.StunPorts\n\t\tif c.Relay.Stun.LogLevel == \"\" {\n\t\t\tc.Relay.Stun.LogLevel = c.Server.LogLevel\n\t\t}\n\t}\n}\n\n// applySignalDefaults configures the signal service if no external signal is configured.\nfunc (c *CombinedConfig) applySignalDefaults(hasExternalSignal bool) {\n\tif hasExternalSignal {\n\t\treturn\n\t}\n\n\tc.Signal.Enabled = true\n\tif c.Signal.LogLevel == \"\" {\n\t\tc.Signal.LogLevel = c.Server.LogLevel\n\t}\n}\n\n// applyManagementDefaults configures the management service (always enabled).\nfunc (c *CombinedConfig) applyManagementDefaults(exposedHost string) {\n\tc.Management.Enabled = true\n\tif c.Management.LogLevel == \"\" {\n\t\tc.Management.LogLevel = c.Server.LogLevel\n\t}\n\tif c.Management.DataDir == \"\" || c.Management.DataDir == \"/var/lib/netbird/\" {\n\t\tc.Management.DataDir = c.Server.DataDir\n\t}\n\tc.Management.DnsDomain = exposedHost\n\tc.Management.DisableAnonymousMetrics = c.Server.DisableAnonymousMetrics\n\tc.Management.DisableGeoliteUpdate = c.Server.DisableGeoliteUpdate\n\t// Copy auth config from server if management auth issuer is not set\n\tif c.Management.Auth.Issuer == \"\" && c.Server.Auth.Issuer != \"\" {\n\t\tc.Management.Auth = c.Server.Auth\n\t}\n\n\t// Copy store config from server if not set\n\tif c.Management.Store.Engine == \"\" || c.Management.Store.Engine == \"sqlite\" {\n\t\tif c.Server.Store.Engine != \"\" {\n\t\t\tc.Management.Store = c.Server.Store\n\t\t}\n\t}\n\n\t// Copy reverse proxy config from server\n\tif len(c.Server.ReverseProxy.TrustedHTTPProxies) > 0 || c.Server.ReverseProxy.TrustedHTTPProxiesCount > 0 || len(c.Server.ReverseProxy.TrustedPeers) > 0 {\n\t\tc.Management.ReverseProxy = c.Server.ReverseProxy\n\t}\n}\n\n// autoConfigureClientSettings sets up STUN/relay/signal URIs for clients\n// External overrides from server config take precedence over auto-generated values\nfunc (c *CombinedConfig) autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort string, hasExternalStuns, hasExternalRelay, hasExternalSignal bool) {\n\t// Determine relay protocol from exposed protocol\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\n\t// Configure STUN servers for clients\n\tif hasExternalStuns {\n\t\t// Use external STUN servers from server config\n\t\tc.Management.Stuns = c.Server.Stuns\n\t} else if len(c.Server.StunPorts) > 0 && len(c.Management.Stuns) == 0 {\n\t\t// Auto-configure local STUN servers for all ports\n\t\tfor _, port := range c.Server.StunPorts {\n\t\t\tc.Management.Stuns = append(c.Management.Stuns, HostConfig{\n\t\t\t\tURI: fmt.Sprintf(\"stun:%s:%d\", exposedHost, port),\n\t\t\t})\n\t\t}\n\t}\n\n\t// Configure relay for clients\n\tif hasExternalRelay {\n\t\t// Use external relay config from server\n\t\tc.Management.Relays = c.Server.Relays\n\t} else if len(c.Management.Relays.Addresses) == 0 {\n\t\t// Auto-configure local relay\n\t\tc.Management.Relays.Addresses = []string{\n\t\t\tfmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort),\n\t\t}\n\t}\n\tif c.Management.Relays.Secret == \"\" {\n\t\tc.Management.Relays.Secret = c.Server.AuthSecret\n\t}\n\tif c.Management.Relays.CredentialsTTL == \"\" {\n\t\tc.Management.Relays.CredentialsTTL = \"12h\"\n\t}\n\n\t// Configure signal for clients\n\tif hasExternalSignal {\n\t\t// Use external signal URI from server config\n\t\tc.Management.SignalURI = c.Server.SignalURI\n\t} else if c.Management.SignalURI == \"\" {\n\t\t// Auto-configure local signal\n\t\tc.Management.SignalURI = fmt.Sprintf(\"%s://%s\", exposedProto, exposedHostPort)\n\t}\n}\n\n// LoadConfig loads configuration from a YAML file\nfunc LoadConfig(configPath string) (*CombinedConfig, error) {\n\tcfg := DefaultConfig()\n\n\tif configPath == \"\" {\n\t\treturn cfg, nil\n\t}\n\n\tdata, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read config file: %w\", err)\n\t}\n\n\tif err := yaml.Unmarshal(data, cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse config file: %w\", err)\n\t}\n\n\t// Populate internal configs from server settings\n\tcfg.ApplySimplifiedDefaults()\n\n\treturn cfg, nil\n}\n\n// Validate validates the configuration\nfunc (c *CombinedConfig) Validate() error {\n\tif c.Server.ExposedAddress == \"\" {\n\t\treturn fmt.Errorf(\"server.exposedAddress is required\")\n\t}\n\tif c.Server.DataDir == \"\" {\n\t\treturn fmt.Errorf(\"server.dataDir is required\")\n\t}\n\n\t// Validate STUN ports\n\tseen := make(map[int]bool)\n\tfor _, port := range c.Server.StunPorts {\n\t\tif port <= 0 || port > 65535 {\n\t\t\treturn fmt.Errorf(\"invalid server.stunPorts value %d: must be between 1 and 65535\", port)\n\t\t}\n\t\tif seen[port] {\n\t\t\treturn fmt.Errorf(\"duplicate STUN port %d in server.stunPorts\", port)\n\t\t}\n\t\tseen[port] = true\n\t}\n\n\t// authSecret is required only if running local relay (no external relay configured)\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\tif !hasExternalRelay && c.Server.AuthSecret == \"\" {\n\t\treturn fmt.Errorf(\"server.authSecret is required when running local relay\")\n\t}\n\n\treturn nil\n}\n\n// HasTLSCert returns true if TLS certificate files are configured\nfunc (c *CombinedConfig) HasTLSCert() bool {\n\treturn c.Server.TLS.CertFile != \"\" && c.Server.TLS.KeyFile != \"\"\n}\n\n// HasLetsEncrypt returns true if Let's Encrypt is configured\nfunc (c *CombinedConfig) HasLetsEncrypt() bool {\n\treturn c.Server.TLS.LetsEncrypt.Enabled &&\n\t\tc.Server.TLS.LetsEncrypt.DataDir != \"\" &&\n\t\tlen(c.Server.TLS.LetsEncrypt.Domains) > 0\n}\n\n// parseExplicitProtocol parses an explicit protocol string to nbconfig.Protocol\nfunc parseExplicitProtocol(proto string) (nbconfig.Protocol, bool) {\n\tswitch strings.ToLower(proto) {\n\tcase \"udp\":\n\t\treturn nbconfig.UDP, true\n\tcase \"dtls\":\n\t\treturn nbconfig.DTLS, true\n\tcase \"tcp\":\n\t\treturn nbconfig.TCP, true\n\tcase \"http\":\n\t\treturn nbconfig.HTTP, true\n\tcase \"https\":\n\t\treturn nbconfig.HTTPS, true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\n// parseStunProtocol determines protocol for STUN/TURN servers.\n// stun: → UDP, stuns: → DTLS, turn: → UDP, turns: → DTLS\n// Explicit proto overrides URI scheme. Defaults to UDP.\nfunc parseStunProtocol(uri, proto string) nbconfig.Protocol {\n\tif proto != \"\" {\n\t\tif p, ok := parseExplicitProtocol(proto); ok {\n\t\t\treturn p\n\t\t}\n\t}\n\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"stuns:\"):\n\t\treturn nbconfig.DTLS\n\tcase strings.HasPrefix(uri, \"turns:\"):\n\t\treturn nbconfig.DTLS\n\tdefault:\n\t\t// stun:, turn:, or no scheme - default to UDP\n\t\treturn nbconfig.UDP\n\t}\n}\n\n// parseSignalProtocol determines protocol for Signal servers.\n// https:// → HTTPS, http:// → HTTP. Defaults to HTTPS.\nfunc parseSignalProtocol(uri string) nbconfig.Protocol {\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"http://\"):\n\t\treturn nbconfig.HTTP\n\tdefault:\n\t\t// https:// or no scheme - default to HTTPS\n\t\treturn nbconfig.HTTPS\n\t}\n}\n\n// stripSignalProtocol removes the protocol prefix from a signal URI.\n// Returns just the host:port (e.g., \"selfhosted2.demo.netbird.io:443\").\nfunc stripSignalProtocol(uri string) string {\n\turi = strings.TrimPrefix(uri, \"https://\")\n\turi = strings.TrimPrefix(uri, \"http://\")\n\treturn uri\n}\n\nfunc buildRelayConfig(relays RelaysConfig) (*nbconfig.Relay, error) {\n\tvar ttl time.Duration\n\tif relays.CredentialsTTL != \"\" {\n\t\tvar err error\n\t\tttl, err = time.ParseDuration(relays.CredentialsTTL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid relay credentials TTL %q: %w\", relays.CredentialsTTL, err)\n\t\t}\n\t}\n\treturn &nbconfig.Relay{\n\t\tAddresses: relays.Addresses,\n\t\tCredentialsTTL: util.Duration{Duration: ttl},\n\t\tSecret: relays.Secret,\n\t}, nil\n}\n\n// buildEmbeddedIdPConfig builds the embedded IdP configuration.\n// authStore overrides auth.storage when set.\nfunc (c *CombinedConfig) buildEmbeddedIdPConfig(mgmt ManagementConfig) (*idp.EmbeddedIdPConfig, error) {\n\tauthStorageType := mgmt.Auth.Storage.Type\n\tauthStorageDSN := c.Server.AuthStore.DSN\n\tif c.Server.AuthStore.Engine != \"\" {\n\t\tauthStorageType = c.Server.AuthStore.Engine\n\t}\n\tif authStorageType == \"\" {\n\t\tauthStorageType = \"sqlite3\"\n\t}\n\tauthStorageFile := \"\"\n\tif authStorageType == \"postgres\" {\n\t\tif authStorageDSN == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"authStore.dsn is required when authStore.engine is postgres\")\n\t\t}\n\t} else {\n\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n\t}\n\n\tcfg := &idp.EmbeddedIdPConfig{\n\t\tEnabled: true,\n\t\tIssuer: mgmt.Auth.Issuer,\n\t\tLocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,\n\t\tSignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,\n\t\tStorage: idp.EmbeddedStorageConfig{\n\t\t\tType: authStorageType,\n\t\t\tConfig: idp.EmbeddedStorageTypeConfig{\n\t\t\t\tFile: authStorageFile,\n\t\t\t\tDSN: authStorageDSN,\n\t\t\t},\n\t\t},\n\t\tDashboardRedirectURIs: mgmt.Auth.DashboardRedirectURIs,\n\t\tCLIRedirectURIs: mgmt.Auth.CLIRedirectURIs,\n\t}\n\n\tif mgmt.Auth.Owner != nil && mgmt.Auth.Owner.Email != \"\" {\n\t\tcfg.Owner = &idp.OwnerConfig{\n\t\t\tEmail: mgmt.Auth.Owner.Email,\n\t\t\tHash: mgmt.Auth.Owner.Password,\n\t\t}\n\t}\n\n\treturn cfg, nil\n}\n\n// ToManagementConfig converts CombinedConfig to management server config\nfunc (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {\n\tmgmt := c.Management\n\n\t// Build STUN hosts\n\tvar stuns []*nbconfig.Host\n\tfor _, s := range mgmt.Stuns {\n\t\tstuns = append(stuns, &nbconfig.Host{\n\t\t\tURI: s.URI,\n\t\t\tProto: parseStunProtocol(s.URI, s.Proto),\n\t\t\tUsername: s.Username,\n\t\t\tPassword: s.Password,\n\t\t})\n\t}\n\n\t// Build relay config\n\tvar relayConfig *nbconfig.Relay\n\tif len(mgmt.Relays.Addresses) > 0 || mgmt.Relays.Secret != \"\" {\n\t\trelay, err := buildRelayConfig(mgmt.Relays)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trelayConfig = relay\n\t}\n\n\t// Build signal config\n\tvar signalConfig *nbconfig.Host\n\tif mgmt.SignalURI != \"\" {\n\t\tsignalConfig = &nbconfig.Host{\n\t\t\tURI: stripSignalProtocol(mgmt.SignalURI),\n\t\t\tProto: parseSignalProtocol(mgmt.SignalURI),\n\t\t}\n\t}\n\n\t// Build store config\n\tstoreConfig := nbconfig.StoreConfig{\n\t\tEngine: types.Engine(mgmt.Store.Engine),\n\t}\n\n\t// Build reverse proxy config\n\treverseProxy := nbconfig.ReverseProxy{\n\t\tTrustedHTTPProxiesCount: mgmt.ReverseProxy.TrustedHTTPProxiesCount,\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedHTTPProxies {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedHTTPProxies = append(reverseProxy.TrustedHTTPProxies, prefix)\n\t\t}\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedPeers {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedPeers = append(reverseProxy.TrustedPeers, prefix)\n\t\t}\n\t}\n\n\t// Build HTTP config (required, even if empty)\n\thttpConfig := &nbconfig.HttpServerConfig{}\n\n\t// Build embedded IDP config (always enabled in combined server)\n\tembeddedIdP, err := c.buildEmbeddedIdPConfig(mgmt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Set HTTP config fields for embedded IDP\n\thttpConfig.AuthIssuer = mgmt.Auth.Issuer\n\thttpConfig.AuthAudience = \"netbird-dashboard\"\n\thttpConfig.AuthClientID = httpConfig.AuthAudience\n\thttpConfig.CLIAuthAudience = \"netbird-cli\"\n\thttpConfig.AuthUserIDClaim = \"sub\"\n\thttpConfig.AuthKeysLocation = mgmt.Auth.Issuer + \"/keys\"\n\thttpConfig.OIDCConfigEndpoint = mgmt.Auth.Issuer + \"/.well-known/openid-configuration\"\n\thttpConfig.IdpSignKeyRefreshEnabled = mgmt.Auth.SignKeyRefreshEnabled\n\tcallbackURL := strings.TrimSuffix(httpConfig.AuthIssuer, \"/oauth2\")\n\thttpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull\n\n\treturn &nbconfig.Config{\n\t\tStuns: stuns,\n\t\tRelay: relayConfig,\n\t\tSignal: signalConfig,\n\t\tDatadir: mgmt.DataDir,\n\t\tDataStoreEncryptionKey: mgmt.Store.EncryptionKey,\n\t\tHttpConfig: httpConfig,\n\t\tStoreConfig: storeConfig,\n\t\tReverseProxy: reverseProxy,\n\t\tDisableDefaultPolicy: mgmt.DisableDefaultPolicy,\n\t\tEmbeddedIdP: embeddedIdP,\n\t}, nil\n}\n\n// ApplyEmbeddedIdPConfig applies embedded IdP configuration to the management config.\n// This mirrors the logic in management/cmd/management.go ApplyEmbeddedIdPConfig.\nfunc ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config, mgmtPort int, disableSingleAccMode bool) error {\n\tif cfg.EmbeddedIdP == nil || !cfg.EmbeddedIdP.Enabled {\n\t\treturn nil\n\t}\n\n\t// Embedded IdP requires single account mode\n\tif disableSingleAccMode {\n\t\treturn fmt.Errorf(\"embedded IdP requires single account mode; multiple account mode is not supported with embedded IdP\")\n\t}\n\n\t// Set LocalAddress for embedded IdP, used for internal JWT validation\n\tcfg.EmbeddedIdP.LocalAddress = fmt.Sprintf(\"localhost:%d\", mgmtPort)\n\n\t// Set storage defaults based on Datadir\n\tif cfg.EmbeddedIdP.Storage.Type == \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Type = \"sqlite3\"\n\t}\n\tif cfg.EmbeddedIdP.Storage.Config.File == \"\" && cfg.Datadir != \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")\n\t}\n\n\tissuer := cfg.EmbeddedIdP.Issuer\n\n\t// Ensure HttpConfig exists\n\tif cfg.HttpConfig == nil {\n\t\tcfg.HttpConfig = &nbconfig.HttpServerConfig{}\n\t}\n\n\t// Set HttpConfig values from EmbeddedIdP\n\tcfg.HttpConfig.AuthIssuer = issuer\n\tcfg.HttpConfig.AuthAudience = \"netbird-dashboard\"\n\tcfg.HttpConfig.CLIAuthAudience = \"netbird-cli\"\n\tcfg.HttpConfig.AuthUserIDClaim = \"sub\"\n\tcfg.HttpConfig.AuthKeysLocation = issuer + \"/keys\"\n\tcfg.HttpConfig.OIDCConfigEndpoint = issuer + \"/.well-known/openid-configuration\"\n\tcfg.HttpConfig.IdpSignKeyRefreshEnabled = true\n\n\treturn nil\n}\n\n// EnsureEncryptionKey generates an encryption key if not set.\n// Unlike management server, we don't write back to the config file.\nfunc EnsureEncryptionKey(ctx context.Context, cfg *nbconfig.Config) error {\n\tif cfg.DataStoreEncryptionKey != \"\" {\n\t\treturn nil\n\t}\n\n\tlog.WithContext(ctx).Infof(\"DataStoreEncryptionKey is not set, generating a new key\")\n\tkey, err := crypt.GenerateKey()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate datastore encryption key: %v\", err)\n\t}\n\tcfg.DataStoreEncryptionKey = key\n\tkeyPreview := key[:8] + \"...\"\n\tlog.WithContext(ctx).Warnf(\"DataStoreEncryptionKey generated (%s); add it to your config file under 'server.store.encryptionKey' to persist across restarts\", keyPreview)\n\n\treturn nil\n}\n\n// LogConfigInfo logs informational messages about the loaded configuration\nfunc LogConfigInfo(cfg *nbconfig.Config) {\n\tif cfg.EmbeddedIdP != nil && cfg.EmbeddedIdP.Enabled {\n\t\tlog.Infof(\"running with the embedded IdP: %v\", cfg.EmbeddedIdP.Issuer)\n\t}\n\tif cfg.Relay != nil {\n\t\tlog.Infof(\"Relay addresses: %v\", cfg.Relay.Addresses)\n\t}\n}","newString":"package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/netbirdio/netbird/management/server/idp\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n\n\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n)\n\n// CombinedConfig is the root configuration for the combined server.\n// The combined server is primarily a Management server with optional embedded\n// Signal, Relay, and STUN services.\n//\n// Architecture:\n// - Management: Always runs locally (this IS the management server)\n// - Signal: Runs locally by default; disabled if server.signalUri is set\n// - Relay: Runs locally by default; disabled if server.relays is set\n// - STUN: Runs locally on port 3478 by default; disabled if server.stuns is set\n//\n// All user-facing settings are under \"server\". The relay/signal/management\n// fields are internal and populated automatically from server settings.\ntype CombinedConfig struct {\n\tServer ServerConfig `yaml:\"server\"`\n\n\t// Internal configs - populated from Server settings, not user-configurable\n\tRelay RelayConfig `yaml:\"-\"`\n\tSignal SignalConfig `yaml:\"-\"`\n\tManagement ManagementConfig `yaml:\"-\"`\n}\n\n// ServerConfig contains server-wide settings\n// In simplified mode, this contains all configuration\ntype ServerConfig struct {\n\tListenAddress string `yaml:\"listenAddress\"`\n\tMetricsPort int `yaml:\"metricsPort\"`\n\tHealthcheckAddress string `yaml:\"healthcheckAddress\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tLogFile string `yaml:\"logFile\"`\n\tTLS TLSConfig `yaml:\"tls\"`\n\n\t// Simplified config fields (used when relay/signal/management sections are omitted)\n\tExposedAddress string `yaml:\"exposedAddress\"` // Public address with protocol (e.g., \"https://example.com:443\")\n\tStunPorts []int `yaml:\"stunPorts\"` // STUN ports (empty to disable local STUN)\n\tAuthSecret string `yaml:\"authSecret\"` // Shared secret for relay authentication\n\tDataDir string `yaml:\"dataDir\"` // Data directory for all services\n\n\t// External service overrides (simplified mode)\n\t// When these are set, the corresponding local service is NOT started\n\t// and these values are used for client configuration instead\n\tStuns []HostConfig `yaml:\"stuns\"` // External STUN servers (disables local STUN)\n\tRelays RelaysConfig `yaml:\"relays\"` // External relay servers (disables local relay)\n\tSignalURI string `yaml:\"signalUri\"` // External signal server (disables local signal)\n\n\t// Management settings (simplified mode)\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tActivityStore StoreConfig `yaml:\"activityStore\"`\n\tAuthStore StoreConfig `yaml:\"authStore\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// TLSConfig contains TLS/HTTPS settings\ntype TLSConfig struct {\n\tCertFile string `yaml:\"certFile\"`\n\tKeyFile string `yaml:\"keyFile\"`\n\tLetsEncrypt LetsEncryptConfig `yaml:\"letsencrypt\"`\n}\n\n// LetsEncryptConfig contains Let's Encrypt settings\ntype LetsEncryptConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDomains []string `yaml:\"domains\"`\n\tEmail string `yaml:\"email\"`\n\tAWSRoute53 bool `yaml:\"awsRoute53\"`\n}\n\n// RelayConfig contains relay service settings\ntype RelayConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tExposedAddress string `yaml:\"exposedAddress\"`\n\tAuthSecret string `yaml:\"authSecret\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tStun StunConfig `yaml:\"stun\"`\n}\n\n// StunConfig contains embedded STUN service settings\ntype StunConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tPorts []int `yaml:\"ports\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// SignalConfig contains signal service settings\ntype SignalConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// ManagementConfig contains management service settings\ntype ManagementConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDnsDomain string `yaml:\"dnsDomain\"`\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tDisableDefaultPolicy bool `yaml:\"disableDefaultPolicy\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStuns []HostConfig `yaml:\"stuns\"`\n\tRelays RelaysConfig `yaml:\"relays\"`\n\tSignalURI string `yaml:\"signalUri\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// AuthConfig contains authentication/identity provider settings\ntype AuthConfig struct {\n\tIssuer string `yaml:\"issuer\"`\n\tLocalAuthDisabled bool `yaml:\"localAuthDisabled\"`\n\tSignKeyRefreshEnabled bool `yaml:\"signKeyRefreshEnabled\"`\n\tStorage AuthStorageConfig `yaml:\"storage\"`\n\tDashboardRedirectURIs []string `yaml:\"dashboardRedirectURIs\"`\n\tCLIRedirectURIs []string `yaml:\"cliRedirectURIs\"`\n\tOwner *AuthOwnerConfig `yaml:\"owner,omitempty\"`\n}\n\n// AuthStorageConfig contains auth storage settings\ntype AuthStorageConfig struct {\n\tType string `yaml:\"type\"`\n\tFile string `yaml:\"file\"`\n}\n\n// AuthOwnerConfig contains initial admin user settings\ntype AuthOwnerConfig struct {\n\tEmail string `yaml:\"email\"`\n\tPassword string `yaml:\"password\"`\n}\n\n// HostConfig represents a STUN/TURN/Signal host\ntype HostConfig struct {\n\tURI string `yaml:\"uri\"`\n\tProto string `yaml:\"proto,omitempty\"` // udp, dtls, tcp, http, https - defaults based on URI scheme\n\tUsername string `yaml:\"username,omitempty\"`\n\tPassword string `yaml:\"password,omitempty\"`\n}\n\n// RelaysConfig contains external relay server settings for clients\ntype RelaysConfig struct {\n\tAddresses []string `yaml:\"addresses\"`\n\tCredentialsTTL string `yaml:\"credentialsTTL\"`\n\tSecret string `yaml:\"secret\"`\n}\n\n// StoreConfig contains database settings\ntype StoreConfig struct {\n\tEngine string `yaml:\"engine\"`\n\tEncryptionKey string `yaml:\"encryptionKey\"`\n\tDSN string `yaml:\"dsn\"` // Connection string for postgres or mysql engines\n\tFile string `yaml:\"file\"` // SQLite database file path (optional, defaults to dataDir)\n}\n\n// ReverseProxyConfig contains reverse proxy settings\ntype ReverseProxyConfig struct {\n\tTrustedHTTPProxies []string `yaml:\"trustedHTTPProxies\"`\n\tTrustedHTTPProxiesCount uint `yaml:\"trustedHTTPProxiesCount\"`\n\tTrustedPeers []string `yaml:\"trustedPeers\"`\n}\n\n// DefaultConfig returns a CombinedConfig with default values\nfunc DefaultConfig() *CombinedConfig {\n\treturn &CombinedConfig{\n\t\tServer: ServerConfig{\n\t\t\tListenAddress: \":443\",\n\t\t\tMetricsPort: 9090,\n\t\t\tHealthcheckAddress: \":9000\",\n\t\t\tLogLevel: \"info\",\n\t\t\tLogFile: \"console\",\n\t\t\tStunPorts: []int{3478},\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t\tRelay: RelayConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\tStun: StunConfig{\n\t\t\t\tEnabled: false,\n\t\t\t\tPorts: []int{3478},\n\t\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\t},\n\t\t},\n\t\tSignal: SignalConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t},\n\t\tManagement: ManagementConfig{\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRelays: RelaysConfig{\n\t\t\t\tCredentialsTTL: \"12h\",\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t}\n}\n\n// hasRequiredSettings returns true if the configuration has the required server settings\nfunc (c *CombinedConfig) hasRequiredSettings() bool {\n\treturn c.Server.ExposedAddress != \"\"\n}\n\n// parseExposedAddress extracts protocol, host, and host:port from the exposed address\n// Input format: \"https://example.com:443\" or \"http://example.com:8080\" or \"example.com:443\"\n// Returns: protocol (\"https\" or \"http\"), hostname only, and host:port\nfunc parseExposedAddress(exposedAddress string) (protocol, hostname, hostPort string) {\n\t// Default to https if no protocol specified\n\tprotocol = \"https\"\n\thostPort = exposedAddress\n\n\t// Check for protocol prefix\n\tif strings.HasPrefix(exposedAddress, \"https://\") {\n\t\tprotocol = \"https\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"https://\")\n\t} else if strings.HasPrefix(exposedAddress, \"http://\") {\n\t\tprotocol = \"http\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"http://\")\n\t}\n\n\t// Extract hostname (without port)\n\thostname = hostPort\n\tif host, _, err := net.SplitHostPort(hostPort); err == nil {\n\t\thostname = host\n\t}\n\n\treturn protocol, hostname, hostPort\n}\n\n// ApplySimplifiedDefaults populates internal relay/signal/management configs from server settings.\n// Management is always enabled. Signal, Relay, and STUN are enabled unless external\n// overrides are configured (server.signalUri, server.relays, server.stuns).\nfunc (c *CombinedConfig) ApplySimplifiedDefaults() {\n\tif !c.hasRequiredSettings() {\n\t\treturn\n\t}\n\n\t// Parse exposed address to extract protocol and hostname\n\texposedProto, exposedHost, exposedHostPort := parseExposedAddress(c.Server.ExposedAddress)\n\n\t// Check for external service overrides\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\thasExternalSignal := c.Server.SignalURI != \"\"\n\thasExternalStuns := len(c.Server.Stuns) > 0\n\n\t// Default stunPorts to [3478] if not specified and no external STUN\n\tif len(c.Server.StunPorts) == 0 && !hasExternalStuns {\n\t\tc.Server.StunPorts = []int{3478}\n\t}\n\n\tc.applyRelayDefaults(exposedProto, exposedHostPort, hasExternalRelay, hasExternalStuns)\n\tc.applySignalDefaults(hasExternalSignal)\n\tc.applyManagementDefaults(exposedHost)\n\n\t// Auto-configure client settings (stuns, relays, signalUri)\n\tc.autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort, hasExternalStuns, hasExternalRelay, hasExternalSignal)\n}\n\n// applyRelayDefaults configures the relay service if no external relay is configured.\nfunc (c *CombinedConfig) applyRelayDefaults(exposedProto, exposedHostPort string, hasExternalRelay, hasExternalStuns bool) {\n\tif hasExternalRelay {\n\t\treturn\n\t}\n\n\tc.Relay.Enabled = true\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\tc.Relay.ExposedAddress = fmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort)\n\tc.Relay.AuthSecret = c.Server.AuthSecret\n\tif c.Relay.LogLevel == \"\" {\n\t\tc.Relay.LogLevel = c.Server.LogLevel\n\t}\n\n\t// Enable local STUN only if no external STUN servers and stunPorts are configured\n\tif !hasExternalStuns && len(c.Server.StunPorts) > 0 {\n\t\tc.Relay.Stun.Enabled = true\n\t\tc.Relay.Stun.Ports = c.Server.StunPorts\n\t\tif c.Relay.Stun.LogLevel == \"\" {\n\t\t\tc.Relay.Stun.LogLevel = c.Server.LogLevel\n\t\t}\n\t}\n}\n\n// applySignalDefaults configures the signal service if no external signal is configured.\nfunc (c *CombinedConfig) applySignalDefaults(hasExternalSignal bool) {\n\tif hasExternalSignal {\n\t\treturn\n\t}\n\n\tc.Signal.Enabled = true\n\tif c.Signal.LogLevel == \"\" {\n\t\tc.Signal.LogLevel = c.Server.LogLevel\n\t}\n}\n\n// applyManagementDefaults configures the management service (always enabled).\nfunc (c *CombinedConfig) applyManagementDefaults(exposedHost string) {\n\tc.Management.Enabled = true\n\tif c.Management.LogLevel == \"\" {\n\t\tc.Management.LogLevel = c.Server.LogLevel\n\t}\n\tif c.Management.DataDir == \"\" || c.Management.DataDir == \"/var/lib/netbird/\" {\n\t\tc.Management.DataDir = c.Server.DataDir\n\t}\n\tc.Management.DnsDomain = exposedHost\n\tc.Management.DisableAnonymousMetrics = c.Server.DisableAnonymousMetrics\n\tc.Management.DisableGeoliteUpdate = c.Server.DisableGeoliteUpdate\n\t// Copy auth config from server if management auth issuer is not set\n\tif c.Management.Auth.Issuer == \"\" && c.Server.Auth.Issuer != \"\" {\n\t\tc.Management.Auth = c.Server.Auth\n\t}\n\n\t// Copy store config from server if not set\n\tif c.Management.Store.Engine == \"\" || c.Management.Store.Engine == \"sqlite\" {\n\t\tif c.Server.Store.Engine != \"\" {\n\t\t\tc.Management.Store = c.Server.Store\n\t\t}\n\t}\n\n\t// Copy reverse proxy config from server\n\tif len(c.Server.ReverseProxy.TrustedHTTPProxies) > 0 || c.Server.ReverseProxy.TrustedHTTPProxiesCount > 0 || len(c.Server.ReverseProxy.TrustedPeers) > 0 {\n\t\tc.Management.ReverseProxy = c.Server.ReverseProxy\n\t}\n}\n\n// autoConfigureClientSettings sets up STUN/relay/signal URIs for clients\n// External overrides from server config take precedence over auto-generated values\nfunc (c *CombinedConfig) autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort string, hasExternalStuns, hasExternalRelay, hasExternalSignal bool) {\n\t// Determine relay protocol from exposed protocol\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\n\t// Configure STUN servers for clients\n\tif hasExternalStuns {\n\t\t// Use external STUN servers from server config\n\t\tc.Management.Stuns = c.Server.Stuns\n\t} else if len(c.Server.StunPorts) > 0 && len(c.Management.Stuns) == 0 {\n\t\t// Auto-configure local STUN servers for all ports\n\t\tfor _, port := range c.Server.StunPorts {\n\t\t\tc.Management.Stuns = append(c.Management.Stuns, HostConfig{\n\t\t\t\tURI: fmt.Sprintf(\"stun:%s:%d\", exposedHost, port),\n\t\t\t})\n\t\t}\n\t}\n\n\t// Configure relay for clients\n\tif hasExternalRelay {\n\t\t// Use external relay config from server\n\t\tc.Management.Relays = c.Server.Relays\n\t} else if len(c.Management.Relays.Addresses) == 0 {\n\t\t// Auto-configure local relay\n\t\tc.Management.Relays.Addresses = []string{\n\t\t\tfmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort),\n\t\t}\n\t}\n\tif c.Management.Relays.Secret == \"\" {\n\t\tc.Management.Relays.Secret = c.Server.AuthSecret\n\t}\n\tif c.Management.Relays.CredentialsTTL == \"\" {\n\t\tc.Management.Relays.CredentialsTTL = \"12h\"\n\t}\n\n\t// Configure signal for clients\n\tif hasExternalSignal {\n\t\t// Use external signal URI from server config\n\t\tc.Management.SignalURI = c.Server.SignalURI\n\t} else if c.Management.SignalURI == \"\" {\n\t\t// Auto-configure local signal\n\t\tc.Management.SignalURI = fmt.Sprintf(\"%s://%s\", exposedProto, exposedHostPort)\n\t}\n}\n\n// LoadConfig loads configuration from a YAML file\nfunc LoadConfig(configPath string) (*CombinedConfig, error) {\n\tcfg := DefaultConfig()\n\n\tif configPath == \"\" {\n\t\treturn cfg, nil\n\t}\n\n\tdata, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read config file: %w\", err)\n\t}\n\n\tif err := yaml.Unmarshal(data, cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse config file: %w\", err)\n\t}\n\n\t// Populate internal configs from server settings\n\tcfg.ApplySimplifiedDefaults()\n\n\treturn cfg, nil\n}\n\n// Validate validates the configuration\nfunc (c *CombinedConfig) Validate() error {\n\tif c.Server.ExposedAddress == \"\" {\n\t\treturn fmt.Errorf(\"server.exposedAddress is required\")\n\t}\n\tif c.Server.DataDir == \"\" {\n\t\treturn fmt.Errorf(\"server.dataDir is required\")\n\t}\n\n\t// Validate STUN ports\n\tseen := make(map[int]bool)\n\tfor _, port := range c.Server.StunPorts {\n\t\tif port <= 0 || port > 65535 {\n\t\t\treturn fmt.Errorf(\"invalid server.stunPorts value %d: must be between 1 and 65535\", port)\n\t\t}\n\t\tif seen[port] {\n\t\t\treturn fmt.Errorf(\"duplicate STUN port %d in server.stunPorts\", port)\n\t\t}\n\t\tseen[port] = true\n\t}\n\n\t// authSecret is required only if running local relay (no external relay configured)\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\tif !hasExternalRelay && c.Server.AuthSecret == \"\" {\n\t\treturn fmt.Errorf(\"server.authSecret is required when running local relay\")\n\t}\n\n\treturn nil\n}\n\n// HasTLSCert returns true if TLS certificate files are configured\nfunc (c *CombinedConfig) HasTLSCert() bool {\n\treturn c.Server.TLS.CertFile != \"\" && c.Server.TLS.KeyFile != \"\"\n}\n\n// HasLetsEncrypt returns true if Let's Encrypt is configured\nfunc (c *CombinedConfig) HasLetsEncrypt() bool {\n\treturn c.Server.TLS.LetsEncrypt.Enabled &&\n\t\tc.Server.TLS.LetsEncrypt.DataDir != \"\" &&\n\t\tlen(c.Server.TLS.LetsEncrypt.Domains) > 0\n}\n\n// parseExplicitProtocol parses an explicit protocol string to nbconfig.Protocol\nfunc parseExplicitProtocol(proto string) (nbconfig.Protocol, bool) {\n\tswitch strings.ToLower(proto) {\n\tcase \"udp\":\n\t\treturn nbconfig.UDP, true\n\tcase \"dtls\":\n\t\treturn nbconfig.DTLS, true\n\tcase \"tcp\":\n\t\treturn nbconfig.TCP, true\n\tcase \"http\":\n\t\treturn nbconfig.HTTP, true\n\tcase \"https\":\n\t\treturn nbconfig.HTTPS, true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\n// parseStunProtocol determines protocol for STUN/TURN servers.\n// stun: → UDP, stuns: → DTLS, turn: → UDP, turns: → DTLS\n// Explicit proto overrides URI scheme. Defaults to UDP.\nfunc parseStunProtocol(uri, proto string) nbconfig.Protocol {\n\tif proto != \"\" {\n\t\tif p, ok := parseExplicitProtocol(proto); ok {\n\t\t\treturn p\n\t\t}\n\t}\n\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"stuns:\"):\n\t\treturn nbconfig.DTLS\n\tcase strings.HasPrefix(uri, \"turns:\"):\n\t\treturn nbconfig.DTLS\n\tdefault:\n\t\t// stun:, turn:, or no scheme - default to UDP\n\t\treturn nbconfig.UDP\n\t}\n}\n\n// parseSignalProtocol determines protocol for Signal servers.\n// https:// → HTTPS, http:// → HTTP. Defaults to HTTPS.\nfunc parseSignalProtocol(uri string) nbconfig.Protocol {\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"http://\"):\n\t\treturn nbconfig.HTTP\n\tdefault:\n\t\t// https:// or no scheme - default to HTTPS\n\t\treturn nbconfig.HTTPS\n\t}\n}\n\n// stripSignalProtocol removes the protocol prefix from a signal URI.\n// Returns just the host:port (e.g., \"selfhosted2.demo.netbird.io:443\").\nfunc stripSignalProtocol(uri string) string {\n\turi = strings.TrimPrefix(uri, \"https://\")\n\turi = strings.TrimPrefix(uri, \"http://\")\n\treturn uri\n}\n\nfunc buildRelayConfig(relays RelaysConfig) (*nbconfig.Relay, error) {\n\tvar ttl time.Duration\n\tif relays.CredentialsTTL != \"\" {\n\t\tvar err error\n\t\tttl, err = time.ParseDuration(relays.CredentialsTTL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid relay credentials TTL %q: %w\", relays.CredentialsTTL, err)\n\t\t}\n\t}\n\treturn &nbconfig.Relay{\n\t\tAddresses: relays.Addresses,\n\t\tCredentialsTTL: util.Duration{Duration: ttl},\n\t\tSecret: relays.Secret,\n\t}, nil\n}\n\n// buildEmbeddedIdPConfig builds the embedded IdP configuration.\n// authStore overrides auth.storage when set.\nfunc (c *CombinedConfig) buildEmbeddedIdPConfig(mgmt ManagementConfig) (*idp.EmbeddedIdPConfig, error) {\n\tauthStorageType := mgmt.Auth.Storage.Type\n\tauthStorageDSN := c.Server.AuthStore.DSN\n\tif c.Server.AuthStore.Engine != \"\" {\n\t\tauthStorageType = c.Server.AuthStore.Engine\n\t}\n\tif authStorageType == \"\" {\n\t\tauthStorageType = \"sqlite3\"\n\t}\n\tauthStorageFile := \"\"\n\tif authStorageType == \"postgres\" {\n\t\tif authStorageDSN == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"authStore.dsn is required when authStore.engine is postgres\")\n\t\t}\n\t} else {\n\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n\t}\n\n\tcfg := &idp.EmbeddedIdPConfig{\n\t\tEnabled: true,\n\t\tIssuer: mgmt.Auth.Issuer,\n\t\tLocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,\n\t\tSignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,\n\t\tStorage: idp.EmbeddedStorageConfig{\n\t\t\tType: authStorageType,\n\t\t\tConfig: idp.EmbeddedStorageTypeConfig{\n\t\t\t\tFile: authStorageFile,\n\t\t\t\tDSN: authStorageDSN,\n\t\t\t},\n\t\t},\n\t\tDashboardRedirectURIs: mgmt.Auth.DashboardRedirectURIs,\n\t\tCLIRedirectURIs: mgmt.Auth.CLIRedirectURIs,\n\t}\n\n\tif mgmt.Auth.Owner != nil && mgmt.Auth.Owner.Email != \"\" {\n\t\tcfg.Owner = &idp.OwnerConfig{\n\t\t\tEmail: mgmt.Auth.Owner.Email,\n\t\t\tHash: mgmt.Auth.Owner.Password,\n\t\t}\n\t}\n\n\treturn cfg, nil\n}\n\n// ToManagementConfig converts CombinedConfig to management server config\nfunc (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {\n\tmgmt := c.Management\n\n\t// Build STUN hosts\n\tvar stuns []*nbconfig.Host\n\tfor _, s := range mgmt.Stuns {\n\t\tstuns = append(stuns, &nbconfig.Host{\n\t\t\tURI: s.URI,\n\t\t\tProto: parseStunProtocol(s.URI, s.Proto),\n\t\t\tUsername: s.Username,\n\t\t\tPassword: s.Password,\n\t\t})\n\t}\n\n\t// Build relay config\n\tvar relayConfig *nbconfig.Relay\n\tif len(mgmt.Relays.Addresses) > 0 || mgmt.Relays.Secret != \"\" {\n\t\trelay, err := buildRelayConfig(mgmt.Relays)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trelayConfig = relay\n\t}\n\n\t// Build signal config\n\tvar signalConfig *nbconfig.Host\n\tif mgmt.SignalURI != \"\" {\n\t\tsignalConfig = &nbconfig.Host{\n\t\t\tURI: stripSignalProtocol(mgmt.SignalURI),\n\t\t\tProto: parseSignalProtocol(mgmt.SignalURI),\n\t\t}\n\t}\n\n\t// Build store config\n\tstoreConfig := nbconfig.StoreConfig{\n\t\tEngine: types.Engine(mgmt.Store.Engine),\n\t}\n\n\t// Build reverse proxy config\n\treverseProxy := nbconfig.ReverseProxy{\n\t\tTrustedHTTPProxiesCount: mgmt.ReverseProxy.TrustedHTTPProxiesCount,\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedHTTPProxies {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedHTTPProxies = append(reverseProxy.TrustedHTTPProxies, prefix)\n\t\t}\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedPeers {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedPeers = append(reverseProxy.TrustedPeers, prefix)\n\t\t}\n\t}\n\n\t// Build HTTP config (required, even if empty)\n\thttpConfig := &nbconfig.HttpServerConfig{}\n\n\t// Build embedded IDP config (always enabled in combined server)\n\tembeddedIdP, err := c.buildEmbeddedIdPConfig(mgmt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Set HTTP config fields for embedded IDP\n\thttpConfig.AuthIssuer = mgmt.Auth.Issuer\n\thttpConfig.AuthAudience = \"netbird-dashboard\"\n\thttpConfig.AuthClientID = httpConfig.AuthAudience\n\thttpConfig.CLIAuthAudience = \"netbird-cli\"\n\thttpConfig.AuthUserIDClaim = \"sub\"\n\thttpConfig.AuthKeysLocation = mgmt.Auth.Issuer + \"/keys\"\n\thttpConfig.OIDCConfigEndpoint = mgmt.Auth.Issuer + \"/.well-known/openid-configuration\"\n\thttpConfig.IdpSignKeyRefreshEnabled = mgmt.Auth.SignKeyRefreshEnabled\n\tcallbackURL := strings.TrimSuffix(httpConfig.AuthIssuer, \"/oauth2\")\n\thttpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull\n\n\treturn &nbconfig.Config{\n\t\tStuns: stuns,\n\t\tRelay: relayConfig,\n\t\tSignal: signalConfig,\n\t\tDatadir: mgmt.DataDir,\n\t\tDataStoreEncryptionKey: mgmt.Store.EncryptionKey,\n\t\tHttpConfig: httpConfig,\n\t\tStoreConfig: storeConfig,\n\t\tReverseProxy: reverseProxy,\n\t\tDisableDefaultPolicy: mgmt.DisableDefaultPolicy,\n\t\tEmbeddedIdP: embeddedIdP,\n\t}, nil\n}\n\n// ApplyEmbeddedIdPConfig applies embedded IdP configuration to the management config.\n// This mirrors the logic in management/cmd/management.go ApplyEmbeddedIdPConfig.\nfunc ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config, mgmtPort int, disableSingleAccMode bool) error {\n\tif cfg.EmbeddedIdP == nil || !cfg.EmbeddedIdP.Enabled {\n\t\treturn nil\n\t}\n\n\t// Embedded IdP requires single account mode\n\tif disableSingleAccMode {\n\t\treturn fmt.Errorf(\"embedded IdP requires single account mode; multiple account mode is not supported with embedded IdP\")\n\t}\n\n\t// Set LocalAddress for embedded IdP, used for internal JWT validation\n\tcfg.EmbeddedIdP.LocalAddress = fmt.Sprintf(\"localhost:%d\", mgmtPort)\n\n\t// Set storage defaults based on Datadir\n\tif cfg.EmbeddedIdP.Storage.Type == \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Type = \"sqlite3\"\n\t}\n\tif cfg.EmbeddedIdP.Storage.Config.File == \"\" && cfg.Datadir != \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")\n\t}\n\n\tissuer := cfg.EmbeddedIdP.Issuer\n\n\t// Ensure HttpConfig exists\n\tif cfg.HttpConfig == nil {\n\t\tcfg.HttpConfig = &nbconfig.HttpServerConfig{}\n\t}\n\n\t// Set HttpConfig values from EmbeddedIdP\n\tcfg.HttpConfig.AuthIssuer = issuer\n\tcfg.HttpConfig.AuthAudience = \"netbird-dashboard\"\n\tcfg.HttpConfig.CLIAuthAudience = \"netbird-cli\"\n\tcfg.HttpConfig.AuthUserIDClaim = \"sub\"\n\tcfg.HttpConfig.AuthKeysLocation = issuer + \"/keys\"\n\tcfg.HttpConfig.OIDCConfigEndpoint = issuer + \"/.well-known/openid-configuration\"\n\tcfg.HttpConfig.IdpSignKeyRefreshEnabled = true\n\n\treturn nil\n}\n\n// EnsureEncryptionKey generates an encryption key if not set.\n// Unlike management server, we don't write back to the config file.\nfunc EnsureEncryptionKey(ctx context.Context, cfg *nbconfig.Config) error {\n\tif cfg.DataStoreEncryptionKey != \"\" {\n\t\treturn nil\n\t}\n\n\tlog.WithContext(ctx).Infof(\"DataStoreEncryptionKey is not set, generating a new key\")\n\tkey, err := crypt.GenerateKey()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate datastore encryption key: %v\", err)\n\t}\n\tcfg.DataStoreEncryptionKey = key\n\tkeyPreview := key[:8] + \"...\"\n\tlog.WithContext(ctx).Warnf(\"DataStoreEncryptionKey generated (%s); add it to your config file under 'server.store.encryptionKey' to persist across restarts\", keyPreview)\n\n\treturn nil\n}\n\n// LogConfigInfo logs informational messages about the loaded configuration\nfunc LogConfigInfo(cfg *nbconfig.Config) {\n\tif cfg.EmbeddedIdP != nil && cfg.EmbeddedIdP.Enabled {\n\t\tlog.Infof(\"running with the embedded IdP: %v\", cfg.EmbeddedIdP.Issuer)\n\t}\n\tif cfg.Relay != nil {\n\t\tlog.Infof(\"Relay addresses: %v\", cfg.Relay.Addresses)\n\t}\n}","originalFile":"package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/netbirdio/netbird/management/server/idp\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n\n\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n)\n\n// CombinedConfig is the root configuration for the combined server.\n// The combined server is primarily a Management server with optional embedded\n// Signal, Relay, and STUN services.\n//\n// Architecture:\n// - Management: Always runs locally (this IS the management server)\n// - Signal: Runs locally by default; disabled if server.signalUri is set\n// - Relay: Runs locally by default; disabled if server.relays is set\n// - STUN: Runs locally on port 3478 by default; disabled if server.stuns is set\n//\n// All user-facing settings are under \"server\". The relay/signal/management\n// fields are internal and populated automatically from server settings.\ntype CombinedConfig struct {\n\tServer ServerConfig `yaml:\"server\"`\n\n\t// Internal configs - populated from Server settings, not user-configurable\n\tRelay RelayConfig `yaml:\"-\"`\n\tSignal SignalConfig `yaml:\"-\"`\n\tManagement ManagementConfig `yaml:\"-\"`\n}\n\n// ServerConfig contains server-wide settings\n// In simplified mode, this contains all configuration\ntype ServerConfig struct {\n\tListenAddress string `yaml:\"listenAddress\"`\n\tMetricsPort int `yaml:\"metricsPort\"`\n\tHealthcheckAddress string `yaml:\"healthcheckAddress\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tLogFile string `yaml:\"logFile\"`\n\tTLS TLSConfig `yaml:\"tls\"`\n\n\t// Simplified config fields (used when relay/signal/management sections are omitted)\n\tExposedAddress string `yaml:\"exposedAddress\"` // Public address with protocol (e.g., \"https://example.com:443\")\n\tStunPorts []int `yaml:\"stunPorts\"` // STUN ports (empty to disable local STUN)\n\tAuthSecret string `yaml:\"authSecret\"` // Shared secret for relay authentication\n\tDataDir string `yaml:\"dataDir\"` // Data directory for all services\n\n\t// External service overrides (simplified mode)\n\t// When these are set, the corresponding local service is NOT started\n\t// and these values are used for client configuration instead\n\tStuns []HostConfig `yaml:\"stuns\"` // External STUN servers (disables local STUN)\n\tRelays RelaysConfig `yaml:\"relays\"` // External relay servers (disables local relay)\n\tSignalURI string `yaml:\"signalUri\"` // External signal server (disables local signal)\n\n\t// Management settings (simplified mode)\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tActivityStore StoreConfig `yaml:\"activityStore\"`\n\tAuthStore StoreConfig `yaml:\"authStore\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// TLSConfig contains TLS/HTTPS settings\ntype TLSConfig struct {\n\tCertFile string `yaml:\"certFile\"`\n\tKeyFile string `yaml:\"keyFile\"`\n\tLetsEncrypt LetsEncryptConfig `yaml:\"letsencrypt\"`\n}\n\n// LetsEncryptConfig contains Let's Encrypt settings\ntype LetsEncryptConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDomains []string `yaml:\"domains\"`\n\tEmail string `yaml:\"email\"`\n\tAWSRoute53 bool `yaml:\"awsRoute53\"`\n}\n\n// RelayConfig contains relay service settings\ntype RelayConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tExposedAddress string `yaml:\"exposedAddress\"`\n\tAuthSecret string `yaml:\"authSecret\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tStun StunConfig `yaml:\"stun\"`\n}\n\n// StunConfig contains embedded STUN service settings\ntype StunConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tPorts []int `yaml:\"ports\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// SignalConfig contains signal service settings\ntype SignalConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// ManagementConfig contains management service settings\ntype ManagementConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDnsDomain string `yaml:\"dnsDomain\"`\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tDisableDefaultPolicy bool `yaml:\"disableDefaultPolicy\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStuns []HostConfig `yaml:\"stuns\"`\n\tRelays RelaysConfig `yaml:\"relays\"`\n\tSignalURI string `yaml:\"signalUri\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// AuthConfig contains authentication/identity provider settings\ntype AuthConfig struct {\n\tIssuer string `yaml:\"issuer\"`\n\tLocalAuthDisabled bool `yaml:\"localAuthDisabled\"`\n\tSignKeyRefreshEnabled bool `yaml:\"signKeyRefreshEnabled\"`\n\tStorage AuthStorageConfig `yaml:\"storage\"`\n\tDashboardRedirectURIs []string `yaml:\"dashboardRedirectURIs\"`\n\tCLIRedirectURIs []string `yaml:\"cliRedirectURIs\"`\n\tOwner *AuthOwnerConfig `yaml:\"owner,omitempty\"`\n}\n\n// AuthStorageConfig contains auth storage settings\ntype AuthStorageConfig struct {\n\tType string `yaml:\"type\"`\n\tFile string `yaml:\"file\"`\n}\n\n// AuthOwnerConfig contains initial admin user settings\ntype AuthOwnerConfig struct {\n\tEmail string `yaml:\"email\"`\n\tPassword string `yaml:\"password\"`\n}\n\n// HostConfig represents a STUN/TURN/Signal host\ntype HostConfig struct {\n\tURI string `yaml:\"uri\"`\n\tProto string `yaml:\"proto,omitempty\"` // udp, dtls, tcp, http, https - defaults based on URI scheme\n\tUsername string `yaml:\"username,omitempty\"`\n\tPassword string `yaml:\"password,omitempty\"`\n}\n\n// RelaysConfig contains external relay server settings for clients\ntype RelaysConfig struct {\n\tAddresses []string `yaml:\"addresses\"`\n\tCredentialsTTL string `yaml:\"credentialsTTL\"`\n\tSecret string `yaml:\"secret\"`\n}\n\n// StoreConfig contains database settings\ntype StoreConfig struct {\n\tEngine string `yaml:\"engine\"`\n\tEncryptionKey string `yaml:\"encryptionKey\"`\n\tDSN string `yaml:\"dsn\"` // Connection string for postgres or mysql engines\n}\n\n// ReverseProxyConfig contains reverse proxy settings\ntype ReverseProxyConfig struct {\n\tTrustedHTTPProxies []string `yaml:\"trustedHTTPProxies\"`\n\tTrustedHTTPProxiesCount uint `yaml:\"trustedHTTPProxiesCount\"`\n\tTrustedPeers []string `yaml:\"trustedPeers\"`\n}\n\n// DefaultConfig returns a CombinedConfig with default values\nfunc DefaultConfig() *CombinedConfig {\n\treturn &CombinedConfig{\n\t\tServer: ServerConfig{\n\t\t\tListenAddress: \":443\",\n\t\t\tMetricsPort: 9090,\n\t\t\tHealthcheckAddress: \":9000\",\n\t\t\tLogLevel: \"info\",\n\t\t\tLogFile: \"console\",\n\t\t\tStunPorts: []int{3478},\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t\tRelay: RelayConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\tStun: StunConfig{\n\t\t\t\tEnabled: false,\n\t\t\t\tPorts: []int{3478},\n\t\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\t},\n\t\t},\n\t\tSignal: SignalConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t},\n\t\tManagement: ManagementConfig{\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRelays: RelaysConfig{\n\t\t\t\tCredentialsTTL: \"12h\",\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t}\n}\n\n// hasRequiredSettings returns true if the configuration has the required server settings\nfunc (c *CombinedConfig) hasRequiredSettings() bool {\n\treturn c.Server.ExposedAddress != \"\"\n}\n\n// parseExposedAddress extracts protocol, host, and host:port from the exposed address\n// Input format: \"https://example.com:443\" or \"http://example.com:8080\" or \"example.com:443\"\n// Returns: protocol (\"https\" or \"http\"), hostname only, and host:port\nfunc parseExposedAddress(exposedAddress string) (protocol, hostname, hostPort string) {\n\t// Default to https if no protocol specified\n\tprotocol = \"https\"\n\thostPort = exposedAddress\n\n\t// Check for protocol prefix\n\tif strings.HasPrefix(exposedAddress, \"https://\") {\n\t\tprotocol = \"https\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"https://\")\n\t} else if strings.HasPrefix(exposedAddress, \"http://\") {\n\t\tprotocol = \"http\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"http://\")\n\t}\n\n\t// Extract hostname (without port)\n\thostname = hostPort\n\tif host, _, err := net.SplitHostPort(hostPort); err == nil {\n\t\thostname = host\n\t}\n\n\treturn protocol, hostname, hostPort\n}\n\n// ApplySimplifiedDefaults populates internal relay/signal/management configs from server settings.\n// Management is always enabled. Signal, Relay, and STUN are enabled unless external\n// overrides are configured (server.signalUri, server.relays, server.stuns).\nfunc (c *CombinedConfig) ApplySimplifiedDefaults() {\n\tif !c.hasRequiredSettings() {\n\t\treturn\n\t}\n\n\t// Parse exposed address to extract protocol and hostname\n\texposedProto, exposedHost, exposedHostPort := parseExposedAddress(c.Server.ExposedAddress)\n\n\t// Check for external service overrides\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\thasExternalSignal := c.Server.SignalURI != \"\"\n\thasExternalStuns := len(c.Server.Stuns) > 0\n\n\t// Default stunPorts to [3478] if not specified and no external STUN\n\tif len(c.Server.StunPorts) == 0 && !hasExternalStuns {\n\t\tc.Server.StunPorts = []int{3478}\n\t}\n\n\tc.applyRelayDefaults(exposedProto, exposedHostPort, hasExternalRelay, hasExternalStuns)\n\tc.applySignalDefaults(hasExternalSignal)\n\tc.applyManagementDefaults(exposedHost)\n\n\t// Auto-configure client settings (stuns, relays, signalUri)\n\tc.autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort, hasExternalStuns, hasExternalRelay, hasExternalSignal)\n}\n\n// applyRelayDefaults configures the relay service if no external relay is configured.\nfunc (c *CombinedConfig) applyRelayDefaults(exposedProto, exposedHostPort string, hasExternalRelay, hasExternalStuns bool) {\n\tif hasExternalRelay {\n\t\treturn\n\t}\n\n\tc.Relay.Enabled = true\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\tc.Relay.ExposedAddress = fmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort)\n\tc.Relay.AuthSecret = c.Server.AuthSecret\n\tif c.Relay.LogLevel == \"\" {\n\t\tc.Relay.LogLevel = c.Server.LogLevel\n\t}\n\n\t// Enable local STUN only if no external STUN servers and stunPorts are configured\n\tif !hasExternalStuns && len(c.Server.StunPorts) > 0 {\n\t\tc.Relay.Stun.Enabled = true\n\t\tc.Relay.Stun.Ports = c.Server.StunPorts\n\t\tif c.Relay.Stun.LogLevel == \"\" {\n\t\t\tc.Relay.Stun.LogLevel = c.Server.LogLevel\n\t\t}\n\t}\n}\n\n// applySignalDefaults configures the signal service if no external signal is configured.\nfunc (c *CombinedConfig) applySignalDefaults(hasExternalSignal bool) {\n\tif hasExternalSignal {\n\t\treturn\n\t}\n\n\tc.Signal.Enabled = true\n\tif c.Signal.LogLevel == \"\" {\n\t\tc.Signal.LogLevel = c.Server.LogLevel\n\t}\n}\n\n// applyManagementDefaults configures the management service (always enabled).\nfunc (c *CombinedConfig) applyManagementDefaults(exposedHost string) {\n\tc.Management.Enabled = true\n\tif c.Management.LogLevel == \"\" {\n\t\tc.Management.LogLevel = c.Server.LogLevel\n\t}\n\tif c.Management.DataDir == \"\" || c.Management.DataDir == \"/var/lib/netbird/\" {\n\t\tc.Management.DataDir = c.Server.DataDir\n\t}\n\tc.Management.DnsDomain = exposedHost\n\tc.Management.DisableAnonymousMetrics = c.Server.DisableAnonymousMetrics\n\tc.Management.DisableGeoliteUpdate = c.Server.DisableGeoliteUpdate\n\t// Copy auth config from server if management auth issuer is not set\n\tif c.Management.Auth.Issuer == \"\" && c.Server.Auth.Issuer != \"\" {\n\t\tc.Management.Auth = c.Server.Auth\n\t}\n\n\t// Copy store config from server if not set\n\tif c.Management.Store.Engine == \"\" || c.Management.Store.Engine == \"sqlite\" {\n\t\tif c.Server.Store.Engine != \"\" {\n\t\t\tc.Management.Store = c.Server.Store\n\t\t}\n\t}\n\n\t// Copy reverse proxy config from server\n\tif len(c.Server.ReverseProxy.TrustedHTTPProxies) > 0 || c.Server.ReverseProxy.TrustedHTTPProxiesCount > 0 || len(c.Server.ReverseProxy.TrustedPeers) > 0 {\n\t\tc.Management.ReverseProxy = c.Server.ReverseProxy\n\t}\n}\n\n// autoConfigureClientSettings sets up STUN/relay/signal URIs for clients\n// External overrides from server config take precedence over auto-generated values\nfunc (c *CombinedConfig) autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort string, hasExternalStuns, hasExternalRelay, hasExternalSignal bool) {\n\t// Determine relay protocol from exposed protocol\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\n\t// Configure STUN servers for clients\n\tif hasExternalStuns {\n\t\t// Use external STUN servers from server config\n\t\tc.Management.Stuns = c.Server.Stuns\n\t} else if len(c.Server.StunPorts) > 0 && len(c.Management.Stuns) == 0 {\n\t\t// Auto-configure local STUN servers for all ports\n\t\tfor _, port := range c.Server.StunPorts {\n\t\t\tc.Management.Stuns = append(c.Management.Stuns, HostConfig{\n\t\t\t\tURI: fmt.Sprintf(\"stun:%s:%d\", exposedHost, port),\n\t\t\t})\n\t\t}\n\t}\n\n\t// Configure relay for clients\n\tif hasExternalRelay {\n\t\t// Use external relay config from server\n\t\tc.Management.Relays = c.Server.Relays\n\t} else if len(c.Management.Relays.Addresses) == 0 {\n\t\t// Auto-configure local relay\n\t\tc.Management.Relays.Addresses = []string{\n\t\t\tfmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort),\n\t\t}\n\t}\n\tif c.Management.Relays.Secret == \"\" {\n\t\tc.Management.Relays.Secret = c.Server.AuthSecret\n\t}\n\tif c.Management.Relays.CredentialsTTL == \"\" {\n\t\tc.Management.Relays.CredentialsTTL = \"12h\"\n\t}\n\n\t// Configure signal for clients\n\tif hasExternalSignal {\n\t\t// Use external signal URI from server config\n\t\tc.Management.SignalURI = c.Server.SignalURI\n\t} else if c.Management.SignalURI == \"\" {\n\t\t// Auto-configure local signal\n\t\tc.Management.SignalURI = fmt.Sprintf(\"%s://%s\", exposedProto, exposedHostPort)\n\t}\n}\n\n// LoadConfig loads configuration from a YAML file\nfunc LoadConfig(configPath string) (*CombinedConfig, error) {\n\tcfg := DefaultConfig()\n\n\tif configPath == \"\" {\n\t\treturn cfg, nil\n\t}\n\n\tdata, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read config file: %w\", err)\n\t}\n\n\tif err := yaml.Unmarshal(data, cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse config file: %w\", err)\n\t}\n\n\t// Populate internal configs from server settings\n\tcfg.ApplySimplifiedDefaults()\n\n\treturn cfg, nil\n}\n\n// Validate validates the configuration\nfunc (c *CombinedConfig) Validate() error {\n\tif c.Server.ExposedAddress == \"\" {\n\t\treturn fmt.Errorf(\"server.exposedAddress is required\")\n\t}\n\tif c.Server.DataDir == \"\" {\n\t\treturn fmt.Errorf(\"server.dataDir is required\")\n\t}\n\n\t// Validate STUN ports\n\tseen := make(map[int]bool)\n\tfor _, port := range c.Server.StunPorts {\n\t\tif port <= 0 || port > 65535 {\n\t\t\treturn fmt.Errorf(\"invalid server.stunPorts value %d: must be between 1 and 65535\", port)\n\t\t}\n\t\tif seen[port] {\n\t\t\treturn fmt.Errorf(\"duplicate STUN port %d in server.stunPorts\", port)\n\t\t}\n\t\tseen[port] = true\n\t}\n\n\t// authSecret is required only if running local relay (no external relay configured)\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\tif !hasExternalRelay && c.Server.AuthSecret == \"\" {\n\t\treturn fmt.Errorf(\"server.authSecret is required when running local relay\")\n\t}\n\n\treturn nil\n}\n\n// HasTLSCert returns true if TLS certificate files are configured\nfunc (c *CombinedConfig) HasTLSCert() bool {\n\treturn c.Server.TLS.CertFile != \"\" && c.Server.TLS.KeyFile != \"\"\n}\n\n// HasLetsEncrypt returns true if Let's Encrypt is configured\nfunc (c *CombinedConfig) HasLetsEncrypt() bool {\n\treturn c.Server.TLS.LetsEncrypt.Enabled &&\n\t\tc.Server.TLS.LetsEncrypt.DataDir != \"\" &&\n\t\tlen(c.Server.TLS.LetsEncrypt.Domains) > 0\n}\n\n// parseExplicitProtocol parses an explicit protocol string to nbconfig.Protocol\nfunc parseExplicitProtocol(proto string) (nbconfig.Protocol, bool) {\n\tswitch strings.ToLower(proto) {\n\tcase \"udp\":\n\t\treturn nbconfig.UDP, true\n\tcase \"dtls\":\n\t\treturn nbconfig.DTLS, true\n\tcase \"tcp\":\n\t\treturn nbconfig.TCP, true\n\tcase \"http\":\n\t\treturn nbconfig.HTTP, true\n\tcase \"https\":\n\t\treturn nbconfig.HTTPS, true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\n// parseStunProtocol determines protocol for STUN/TURN servers.\n// stun: → UDP, stuns: → DTLS, turn: → UDP, turns: → DTLS\n// Explicit proto overrides URI scheme. Defaults to UDP.\nfunc parseStunProtocol(uri, proto string) nbconfig.Protocol {\n\tif proto != \"\" {\n\t\tif p, ok := parseExplicitProtocol(proto); ok {\n\t\t\treturn p\n\t\t}\n\t}\n\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"stuns:\"):\n\t\treturn nbconfig.DTLS\n\tcase strings.HasPrefix(uri, \"turns:\"):\n\t\treturn nbconfig.DTLS\n\tdefault:\n\t\t// stun:, turn:, or no scheme - default to UDP\n\t\treturn nbconfig.UDP\n\t}\n}\n\n// parseSignalProtocol determines protocol for Signal servers.\n// https:// → HTTPS, http:// → HTTP. Defaults to HTTPS.\nfunc parseSignalProtocol(uri string) nbconfig.Protocol {\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"http://\"):\n\t\treturn nbconfig.HTTP\n\tdefault:\n\t\t// https:// or no scheme - default to HTTPS\n\t\treturn nbconfig.HTTPS\n\t}\n}\n\n// stripSignalProtocol removes the protocol prefix from a signal URI.\n// Returns just the host:port (e.g., \"selfhosted2.demo.netbird.io:443\").\nfunc stripSignalProtocol(uri string) string {\n\turi = strings.TrimPrefix(uri, \"https://\")\n\turi = strings.TrimPrefix(uri, \"http://\")\n\treturn uri\n}\n\nfunc buildRelayConfig(relays RelaysConfig) (*nbconfig.Relay, error) {\n\tvar ttl time.Duration\n\tif relays.CredentialsTTL != \"\" {\n\t\tvar err error\n\t\tttl, err = time.ParseDuration(relays.CredentialsTTL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid relay credentials TTL %q: %w\", relays.CredentialsTTL, err)\n\t\t}\n\t}\n\treturn &nbconfig.Relay{\n\t\tAddresses: relays.Addresses,\n\t\tCredentialsTTL: util.Duration{Duration: ttl},\n\t\tSecret: relays.Secret,\n\t}, nil\n}\n\n// buildEmbeddedIdPConfig builds the embedded IdP configuration.\n// authStore overrides auth.storage when set.\nfunc (c *CombinedConfig) buildEmbeddedIdPConfig(mgmt ManagementConfig) (*idp.EmbeddedIdPConfig, error) {\n\tauthStorageType := mgmt.Auth.Storage.Type\n\tauthStorageDSN := c.Server.AuthStore.DSN\n\tif c.Server.AuthStore.Engine != \"\" {\n\t\tauthStorageType = c.Server.AuthStore.Engine\n\t}\n\tif authStorageType == \"\" {\n\t\tauthStorageType = \"sqlite3\"\n\t}\n\tauthStorageFile := \"\"\n\tif authStorageType == \"postgres\" {\n\t\tif authStorageDSN == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"authStore.dsn is required when authStore.engine is postgres\")\n\t\t}\n\t} else {\n\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n\t}\n\n\tcfg := &idp.EmbeddedIdPConfig{\n\t\tEnabled: true,\n\t\tIssuer: mgmt.Auth.Issuer,\n\t\tLocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,\n\t\tSignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,\n\t\tStorage: idp.EmbeddedStorageConfig{\n\t\t\tType: authStorageType,\n\t\t\tConfig: idp.EmbeddedStorageTypeConfig{\n\t\t\t\tFile: authStorageFile,\n\t\t\t\tDSN: authStorageDSN,\n\t\t\t},\n\t\t},\n\t\tDashboardRedirectURIs: mgmt.Auth.DashboardRedirectURIs,\n\t\tCLIRedirectURIs: mgmt.Auth.CLIRedirectURIs,\n\t}\n\n\tif mgmt.Auth.Owner != nil && mgmt.Auth.Owner.Email != \"\" {\n\t\tcfg.Owner = &idp.OwnerConfig{\n\t\t\tEmail: mgmt.Auth.Owner.Email,\n\t\t\tHash: mgmt.Auth.Owner.Password,\n\t\t}\n\t}\n\n\treturn cfg, nil\n}\n\n// ToManagementConfig converts CombinedConfig to management server config\nfunc (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {\n\tmgmt := c.Management\n\n\t// Build STUN hosts\n\tvar stuns []*nbconfig.Host\n\tfor _, s := range mgmt.Stuns {\n\t\tstuns = append(stuns, &nbconfig.Host{\n\t\t\tURI: s.URI,\n\t\t\tProto: parseStunProtocol(s.URI, s.Proto),\n\t\t\tUsername: s.Username,\n\t\t\tPassword: s.Password,\n\t\t})\n\t}\n\n\t// Build relay config\n\tvar relayConfig *nbconfig.Relay\n\tif len(mgmt.Relays.Addresses) > 0 || mgmt.Relays.Secret != \"\" {\n\t\trelay, err := buildRelayConfig(mgmt.Relays)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trelayConfig = relay\n\t}\n\n\t// Build signal config\n\tvar signalConfig *nbconfig.Host\n\tif mgmt.SignalURI != \"\" {\n\t\tsignalConfig = &nbconfig.Host{\n\t\t\tURI: stripSignalProtocol(mgmt.SignalURI),\n\t\t\tProto: parseSignalProtocol(mgmt.SignalURI),\n\t\t}\n\t}\n\n\t// Build store config\n\tstoreConfig := nbconfig.StoreConfig{\n\t\tEngine: types.Engine(mgmt.Store.Engine),\n\t}\n\n\t// Build reverse proxy config\n\treverseProxy := nbconfig.ReverseProxy{\n\t\tTrustedHTTPProxiesCount: mgmt.ReverseProxy.TrustedHTTPProxiesCount,\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedHTTPProxies {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedHTTPProxies = append(reverseProxy.TrustedHTTPProxies, prefix)\n\t\t}\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedPeers {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedPeers = append(reverseProxy.TrustedPeers, prefix)\n\t\t}\n\t}\n\n\t// Build HTTP config (required, even if empty)\n\thttpConfig := &nbconfig.HttpServerConfig{}\n\n\t// Build embedded IDP config (always enabled in combined server)\n\tembeddedIdP, err := c.buildEmbeddedIdPConfig(mgmt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Set HTTP config fields for embedded IDP\n\thttpConfig.AuthIssuer = mgmt.Auth.Issuer\n\thttpConfig.AuthAudience = \"netbird-dashboard\"\n\thttpConfig.AuthClientID = httpConfig.AuthAudience\n\thttpConfig.CLIAuthAudience = \"netbird-cli\"\n\thttpConfig.AuthUserIDClaim = \"sub\"\n\thttpConfig.AuthKeysLocation = mgmt.Auth.Issuer + \"/keys\"\n\thttpConfig.OIDCConfigEndpoint = mgmt.Auth.Issuer + \"/.well-known/openid-configuration\"\n\thttpConfig.IdpSignKeyRefreshEnabled = mgmt.Auth.SignKeyRefreshEnabled\n\tcallbackURL := strings.TrimSuffix(httpConfig.AuthIssuer, \"/oauth2\")\n\thttpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull\n\n\treturn &nbconfig.Config{\n\t\tStuns: stuns,\n\t\tRelay: relayConfig,\n\t\tSignal: signalConfig,\n\t\tDatadir: mgmt.DataDir,\n\t\tDataStoreEncryptionKey: mgmt.Store.EncryptionKey,\n\t\tHttpConfig: httpConfig,\n\t\tStoreConfig: storeConfig,\n\t\tReverseProxy: reverseProxy,\n\t\tDisableDefaultPolicy: mgmt.DisableDefaultPolicy,\n\t\tEmbeddedIdP: embeddedIdP,\n\t}, nil\n}\n\n// ApplyEmbeddedIdPConfig applies embedded IdP configuration to the management config.\n// This mirrors the logic in management/cmd/management.go ApplyEmbeddedIdPConfig.\nfunc ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config, mgmtPort int, disableSingleAccMode bool) error {\n\tif cfg.EmbeddedIdP == nil || !cfg.EmbeddedIdP.Enabled {\n\t\treturn nil\n\t}\n\n\t// Embedded IdP requires single account mode\n\tif disableSingleAccMode {\n\t\treturn fmt.Errorf(\"embedded IdP requires single account mode; multiple account mode is not supported with embedded IdP\")\n\t}\n\n\t// Set LocalAddress for embedded IdP, used for internal JWT validation\n\tcfg.EmbeddedIdP.LocalAddress = fmt.Sprintf(\"localhost:%d\", mgmtPort)\n\n\t// Set storage defaults based on Datadir\n\tif cfg.EmbeddedIdP.Storage.Type == \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Type = \"sqlite3\"\n\t}\n\tif cfg.EmbeddedIdP.Storage.Config.File == \"\" && cfg.Datadir != \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")\n\t}\n\n\tissuer := cfg.EmbeddedIdP.Issuer\n\n\t// Ensure HttpConfig exists\n\tif cfg.HttpConfig == nil {\n\t\tcfg.HttpConfig = &nbconfig.HttpServerConfig{}\n\t}\n\n\t// Set HttpConfig values from EmbeddedIdP\n\tcfg.HttpConfig.AuthIssuer = issuer\n\tcfg.HttpConfig.AuthAudience = \"netbird-dashboard\"\n\tcfg.HttpConfig.CLIAuthAudience = \"netbird-cli\"\n\tcfg.HttpConfig.AuthUserIDClaim = \"sub\"\n\tcfg.HttpConfig.AuthKeysLocation = issuer + \"/keys\"\n\tcfg.HttpConfig.OIDCConfigEndpoint = issuer + \"/.well-known/openid-configuration\"\n\tcfg.HttpConfig.IdpSignKeyRefreshEnabled = true\n\n\treturn nil\n}\n\n// EnsureEncryptionKey generates an encryption key if not set.\n// Unlike management server, we don't write back to the config file.\nfunc EnsureEncryptionKey(ctx context.Context, cfg *nbconfig.Config) error {\n\tif cfg.DataStoreEncryptionKey != \"\" {\n\t\treturn nil\n\t}\n\n\tlog.WithContext(ctx).Infof(\"DataStoreEncryptionKey is not set, generating a new key\")\n\tkey, err := crypt.GenerateKey()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate datastore encryption key: %v\", err)\n\t}\n\tcfg.DataStoreEncryptionKey = key\n\tkeyPreview := key[:8] + \"...\"\n\tlog.WithContext(ctx).Warnf(\"DataStoreEncryptionKey generated (%s); add it to your config file under 'server.store.encryptionKey' to persist across restarts\", keyPreview)\n\n\treturn nil\n}\n\n// LogConfigInfo logs informational messages about the loaded configuration\nfunc LogConfigInfo(cfg *nbconfig.Config) {\n\tif cfg.EmbeddedIdP != nil && cfg.EmbeddedIdP.Enabled {\n\t\tlog.Infof(\"running with the embedded IdP: %v\", cfg.EmbeddedIdP.Issuer)\n\t}\n\tif cfg.Relay != nil {\n\t\tlog.Infof(\"Relay addresses: %v\", cfg.Relay.Addresses)\n\t}\n}\n","structuredPatch":[{"oldStart":172,"oldLines":7,"newStart":172,"newLines":8,"lines":[" type StoreConfig struct {"," Engine string `yaml:\"engine\"`"," EncryptionKey string `yaml:\"encryptionKey\"`","- DSN string `yaml:\"dsn\"` // Connection string for postgres or mysql engines","+ DSN string `yaml:\"dsn\"` // Connection string for postgres or mysql engines","+ File string `yaml:\"file\"` // SQLite database file path (optional, defaults to dataDir)"," }"," "," // ReverseProxyConfig contains reverse proxy settings"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"a349ca91-b995-4a12-96e6-2d7cd7c9b8b0"} +{"parentUuid":"38b3c5e1-bfac-4fa9-a813-52fcc6575800","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01YYo1Wu3bYBRK4K9Rxf9YJU","type":"message","role":"assistant","content":[{"type":"text","text":"Now update `buildEmbeddedIdPConfig` to use the authStore `File` field when set:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":460,"cache_read_input_tokens":72884,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":460},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYenGu7wSnd1qY4mNtDog","type":"assistant","uuid":"091ddc84-fa45-4073-b9b2-988432e2ff6d","timestamp":"2026-03-02T19:09:56.423Z"} +{"parentUuid":"091ddc84-fa45-4073-b9b2-988432e2ff6d","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01YYo1Wu3bYBRK4K9Rxf9YJU","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01DXegpfdnF2wBSpEEUwUoH8","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go","old_string":"\t} else {\n\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n\t}","new_string":"\t} else {\n\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n\t\tif c.Server.AuthStore.File != \"\" {\n\t\t\tauthStorageFile = c.Server.AuthStore.File\n\t\t}\n\t}"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":460,"cache_read_input_tokens":72884,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":460},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYenGu7wSnd1qY4mNtDog","type":"assistant","uuid":"86b41705-73f9-4703-b63c-8ec2a36b1c3a","timestamp":"2026-03-02T19:09:58.943Z"} +{"parentUuid":"86b41705-73f9-4703-b63c-8ec2a36b1c3a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_01DXegpfdnF2wBSpEEUwUoH8","toolUseID":"toolu_01DXegpfdnF2wBSpEEUwUoH8","timestamp":"2026-03-02T19:10:15.130Z","uuid":"73b213cc-b8b9-4193-9172-3e10e320c3ad"} +{"parentUuid":"86b41705-73f9-4703-b63c-8ec2a36b1c3a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01DXegpfdnF2wBSpEEUwUoH8","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go has been updated successfully."}]},"uuid":"7a02d6d6-16b4-4eb1-8e57-d3e55036f01b","timestamp":"2026-03-02T19:10:15.128Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go","oldString":"package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/netbirdio/netbird/management/server/idp\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n\n\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n)\n\n// CombinedConfig is the root configuration for the combined server.\n// The combined server is primarily a Management server with optional embedded\n// Signal, Relay, and STUN services.\n//\n// Architecture:\n// - Management: Always runs locally (this IS the management server)\n// - Signal: Runs locally by default; disabled if server.signalUri is set\n// - Relay: Runs locally by default; disabled if server.relays is set\n// - STUN: Runs locally on port 3478 by default; disabled if server.stuns is set\n//\n// All user-facing settings are under \"server\". The relay/signal/management\n// fields are internal and populated automatically from server settings.\ntype CombinedConfig struct {\n\tServer ServerConfig `yaml:\"server\"`\n\n\t// Internal configs - populated from Server settings, not user-configurable\n\tRelay RelayConfig `yaml:\"-\"`\n\tSignal SignalConfig `yaml:\"-\"`\n\tManagement ManagementConfig `yaml:\"-\"`\n}\n\n// ServerConfig contains server-wide settings\n// In simplified mode, this contains all configuration\ntype ServerConfig struct {\n\tListenAddress string `yaml:\"listenAddress\"`\n\tMetricsPort int `yaml:\"metricsPort\"`\n\tHealthcheckAddress string `yaml:\"healthcheckAddress\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tLogFile string `yaml:\"logFile\"`\n\tTLS TLSConfig `yaml:\"tls\"`\n\n\t// Simplified config fields (used when relay/signal/management sections are omitted)\n\tExposedAddress string `yaml:\"exposedAddress\"` // Public address with protocol (e.g., \"https://example.com:443\")\n\tStunPorts []int `yaml:\"stunPorts\"` // STUN ports (empty to disable local STUN)\n\tAuthSecret string `yaml:\"authSecret\"` // Shared secret for relay authentication\n\tDataDir string `yaml:\"dataDir\"` // Data directory for all services\n\n\t// External service overrides (simplified mode)\n\t// When these are set, the corresponding local service is NOT started\n\t// and these values are used for client configuration instead\n\tStuns []HostConfig `yaml:\"stuns\"` // External STUN servers (disables local STUN)\n\tRelays RelaysConfig `yaml:\"relays\"` // External relay servers (disables local relay)\n\tSignalURI string `yaml:\"signalUri\"` // External signal server (disables local signal)\n\n\t// Management settings (simplified mode)\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tActivityStore StoreConfig `yaml:\"activityStore\"`\n\tAuthStore StoreConfig `yaml:\"authStore\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// TLSConfig contains TLS/HTTPS settings\ntype TLSConfig struct {\n\tCertFile string `yaml:\"certFile\"`\n\tKeyFile string `yaml:\"keyFile\"`\n\tLetsEncrypt LetsEncryptConfig `yaml:\"letsencrypt\"`\n}\n\n// LetsEncryptConfig contains Let's Encrypt settings\ntype LetsEncryptConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDomains []string `yaml:\"domains\"`\n\tEmail string `yaml:\"email\"`\n\tAWSRoute53 bool `yaml:\"awsRoute53\"`\n}\n\n// RelayConfig contains relay service settings\ntype RelayConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tExposedAddress string `yaml:\"exposedAddress\"`\n\tAuthSecret string `yaml:\"authSecret\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tStun StunConfig `yaml:\"stun\"`\n}\n\n// StunConfig contains embedded STUN service settings\ntype StunConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tPorts []int `yaml:\"ports\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// SignalConfig contains signal service settings\ntype SignalConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// ManagementConfig contains management service settings\ntype ManagementConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDnsDomain string `yaml:\"dnsDomain\"`\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tDisableDefaultPolicy bool `yaml:\"disableDefaultPolicy\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStuns []HostConfig `yaml:\"stuns\"`\n\tRelays RelaysConfig `yaml:\"relays\"`\n\tSignalURI string `yaml:\"signalUri\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// AuthConfig contains authentication/identity provider settings\ntype AuthConfig struct {\n\tIssuer string `yaml:\"issuer\"`\n\tLocalAuthDisabled bool `yaml:\"localAuthDisabled\"`\n\tSignKeyRefreshEnabled bool `yaml:\"signKeyRefreshEnabled\"`\n\tStorage AuthStorageConfig `yaml:\"storage\"`\n\tDashboardRedirectURIs []string `yaml:\"dashboardRedirectURIs\"`\n\tCLIRedirectURIs []string `yaml:\"cliRedirectURIs\"`\n\tOwner *AuthOwnerConfig `yaml:\"owner,omitempty\"`\n}\n\n// AuthStorageConfig contains auth storage settings\ntype AuthStorageConfig struct {\n\tType string `yaml:\"type\"`\n\tFile string `yaml:\"file\"`\n}\n\n// AuthOwnerConfig contains initial admin user settings\ntype AuthOwnerConfig struct {\n\tEmail string `yaml:\"email\"`\n\tPassword string `yaml:\"password\"`\n}\n\n// HostConfig represents a STUN/TURN/Signal host\ntype HostConfig struct {\n\tURI string `yaml:\"uri\"`\n\tProto string `yaml:\"proto,omitempty\"` // udp, dtls, tcp, http, https - defaults based on URI scheme\n\tUsername string `yaml:\"username,omitempty\"`\n\tPassword string `yaml:\"password,omitempty\"`\n}\n\n// RelaysConfig contains external relay server settings for clients\ntype RelaysConfig struct {\n\tAddresses []string `yaml:\"addresses\"`\n\tCredentialsTTL string `yaml:\"credentialsTTL\"`\n\tSecret string `yaml:\"secret\"`\n}\n\n// StoreConfig contains database settings\ntype StoreConfig struct {\n\tEngine string `yaml:\"engine\"`\n\tEncryptionKey string `yaml:\"encryptionKey\"`\n\tDSN string `yaml:\"dsn\"` // Connection string for postgres or mysql engines\n\tFile string `yaml:\"file\"` // SQLite database file path (optional, defaults to dataDir)\n}\n\n// ReverseProxyConfig contains reverse proxy settings\ntype ReverseProxyConfig struct {\n\tTrustedHTTPProxies []string `yaml:\"trustedHTTPProxies\"`\n\tTrustedHTTPProxiesCount uint `yaml:\"trustedHTTPProxiesCount\"`\n\tTrustedPeers []string `yaml:\"trustedPeers\"`\n}\n\n// DefaultConfig returns a CombinedConfig with default values\nfunc DefaultConfig() *CombinedConfig {\n\treturn &CombinedConfig{\n\t\tServer: ServerConfig{\n\t\t\tListenAddress: \":443\",\n\t\t\tMetricsPort: 9090,\n\t\t\tHealthcheckAddress: \":9000\",\n\t\t\tLogLevel: \"info\",\n\t\t\tLogFile: \"console\",\n\t\t\tStunPorts: []int{3478},\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t\tRelay: RelayConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\tStun: StunConfig{\n\t\t\t\tEnabled: false,\n\t\t\t\tPorts: []int{3478},\n\t\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\t},\n\t\t},\n\t\tSignal: SignalConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t},\n\t\tManagement: ManagementConfig{\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRelays: RelaysConfig{\n\t\t\t\tCredentialsTTL: \"12h\",\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t}\n}\n\n// hasRequiredSettings returns true if the configuration has the required server settings\nfunc (c *CombinedConfig) hasRequiredSettings() bool {\n\treturn c.Server.ExposedAddress != \"\"\n}\n\n// parseExposedAddress extracts protocol, host, and host:port from the exposed address\n// Input format: \"https://example.com:443\" or \"http://example.com:8080\" or \"example.com:443\"\n// Returns: protocol (\"https\" or \"http\"), hostname only, and host:port\nfunc parseExposedAddress(exposedAddress string) (protocol, hostname, hostPort string) {\n\t// Default to https if no protocol specified\n\tprotocol = \"https\"\n\thostPort = exposedAddress\n\n\t// Check for protocol prefix\n\tif strings.HasPrefix(exposedAddress, \"https://\") {\n\t\tprotocol = \"https\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"https://\")\n\t} else if strings.HasPrefix(exposedAddress, \"http://\") {\n\t\tprotocol = \"http\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"http://\")\n\t}\n\n\t// Extract hostname (without port)\n\thostname = hostPort\n\tif host, _, err := net.SplitHostPort(hostPort); err == nil {\n\t\thostname = host\n\t}\n\n\treturn protocol, hostname, hostPort\n}\n\n// ApplySimplifiedDefaults populates internal relay/signal/management configs from server settings.\n// Management is always enabled. Signal, Relay, and STUN are enabled unless external\n// overrides are configured (server.signalUri, server.relays, server.stuns).\nfunc (c *CombinedConfig) ApplySimplifiedDefaults() {\n\tif !c.hasRequiredSettings() {\n\t\treturn\n\t}\n\n\t// Parse exposed address to extract protocol and hostname\n\texposedProto, exposedHost, exposedHostPort := parseExposedAddress(c.Server.ExposedAddress)\n\n\t// Check for external service overrides\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\thasExternalSignal := c.Server.SignalURI != \"\"\n\thasExternalStuns := len(c.Server.Stuns) > 0\n\n\t// Default stunPorts to [3478] if not specified and no external STUN\n\tif len(c.Server.StunPorts) == 0 && !hasExternalStuns {\n\t\tc.Server.StunPorts = []int{3478}\n\t}\n\n\tc.applyRelayDefaults(exposedProto, exposedHostPort, hasExternalRelay, hasExternalStuns)\n\tc.applySignalDefaults(hasExternalSignal)\n\tc.applyManagementDefaults(exposedHost)\n\n\t// Auto-configure client settings (stuns, relays, signalUri)\n\tc.autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort, hasExternalStuns, hasExternalRelay, hasExternalSignal)\n}\n\n// applyRelayDefaults configures the relay service if no external relay is configured.\nfunc (c *CombinedConfig) applyRelayDefaults(exposedProto, exposedHostPort string, hasExternalRelay, hasExternalStuns bool) {\n\tif hasExternalRelay {\n\t\treturn\n\t}\n\n\tc.Relay.Enabled = true\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\tc.Relay.ExposedAddress = fmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort)\n\tc.Relay.AuthSecret = c.Server.AuthSecret\n\tif c.Relay.LogLevel == \"\" {\n\t\tc.Relay.LogLevel = c.Server.LogLevel\n\t}\n\n\t// Enable local STUN only if no external STUN servers and stunPorts are configured\n\tif !hasExternalStuns && len(c.Server.StunPorts) > 0 {\n\t\tc.Relay.Stun.Enabled = true\n\t\tc.Relay.Stun.Ports = c.Server.StunPorts\n\t\tif c.Relay.Stun.LogLevel == \"\" {\n\t\t\tc.Relay.Stun.LogLevel = c.Server.LogLevel\n\t\t}\n\t}\n}\n\n// applySignalDefaults configures the signal service if no external signal is configured.\nfunc (c *CombinedConfig) applySignalDefaults(hasExternalSignal bool) {\n\tif hasExternalSignal {\n\t\treturn\n\t}\n\n\tc.Signal.Enabled = true\n\tif c.Signal.LogLevel == \"\" {\n\t\tc.Signal.LogLevel = c.Server.LogLevel\n\t}\n}\n\n// applyManagementDefaults configures the management service (always enabled).\nfunc (c *CombinedConfig) applyManagementDefaults(exposedHost string) {\n\tc.Management.Enabled = true\n\tif c.Management.LogLevel == \"\" {\n\t\tc.Management.LogLevel = c.Server.LogLevel\n\t}\n\tif c.Management.DataDir == \"\" || c.Management.DataDir == \"/var/lib/netbird/\" {\n\t\tc.Management.DataDir = c.Server.DataDir\n\t}\n\tc.Management.DnsDomain = exposedHost\n\tc.Management.DisableAnonymousMetrics = c.Server.DisableAnonymousMetrics\n\tc.Management.DisableGeoliteUpdate = c.Server.DisableGeoliteUpdate\n\t// Copy auth config from server if management auth issuer is not set\n\tif c.Management.Auth.Issuer == \"\" && c.Server.Auth.Issuer != \"\" {\n\t\tc.Management.Auth = c.Server.Auth\n\t}\n\n\t// Copy store config from server if not set\n\tif c.Management.Store.Engine == \"\" || c.Management.Store.Engine == \"sqlite\" {\n\t\tif c.Server.Store.Engine != \"\" {\n\t\t\tc.Management.Store = c.Server.Store\n\t\t}\n\t}\n\n\t// Copy reverse proxy config from server\n\tif len(c.Server.ReverseProxy.TrustedHTTPProxies) > 0 || c.Server.ReverseProxy.TrustedHTTPProxiesCount > 0 || len(c.Server.ReverseProxy.TrustedPeers) > 0 {\n\t\tc.Management.ReverseProxy = c.Server.ReverseProxy\n\t}\n}\n\n// autoConfigureClientSettings sets up STUN/relay/signal URIs for clients\n// External overrides from server config take precedence over auto-generated values\nfunc (c *CombinedConfig) autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort string, hasExternalStuns, hasExternalRelay, hasExternalSignal bool) {\n\t// Determine relay protocol from exposed protocol\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\n\t// Configure STUN servers for clients\n\tif hasExternalStuns {\n\t\t// Use external STUN servers from server config\n\t\tc.Management.Stuns = c.Server.Stuns\n\t} else if len(c.Server.StunPorts) > 0 && len(c.Management.Stuns) == 0 {\n\t\t// Auto-configure local STUN servers for all ports\n\t\tfor _, port := range c.Server.StunPorts {\n\t\t\tc.Management.Stuns = append(c.Management.Stuns, HostConfig{\n\t\t\t\tURI: fmt.Sprintf(\"stun:%s:%d\", exposedHost, port),\n\t\t\t})\n\t\t}\n\t}\n\n\t// Configure relay for clients\n\tif hasExternalRelay {\n\t\t// Use external relay config from server\n\t\tc.Management.Relays = c.Server.Relays\n\t} else if len(c.Management.Relays.Addresses) == 0 {\n\t\t// Auto-configure local relay\n\t\tc.Management.Relays.Addresses = []string{\n\t\t\tfmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort),\n\t\t}\n\t}\n\tif c.Management.Relays.Secret == \"\" {\n\t\tc.Management.Relays.Secret = c.Server.AuthSecret\n\t}\n\tif c.Management.Relays.CredentialsTTL == \"\" {\n\t\tc.Management.Relays.CredentialsTTL = \"12h\"\n\t}\n\n\t// Configure signal for clients\n\tif hasExternalSignal {\n\t\t// Use external signal URI from server config\n\t\tc.Management.SignalURI = c.Server.SignalURI\n\t} else if c.Management.SignalURI == \"\" {\n\t\t// Auto-configure local signal\n\t\tc.Management.SignalURI = fmt.Sprintf(\"%s://%s\", exposedProto, exposedHostPort)\n\t}\n}\n\n// LoadConfig loads configuration from a YAML file\nfunc LoadConfig(configPath string) (*CombinedConfig, error) {\n\tcfg := DefaultConfig()\n\n\tif configPath == \"\" {\n\t\treturn cfg, nil\n\t}\n\n\tdata, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read config file: %w\", err)\n\t}\n\n\tif err := yaml.Unmarshal(data, cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse config file: %w\", err)\n\t}\n\n\t// Populate internal configs from server settings\n\tcfg.ApplySimplifiedDefaults()\n\n\treturn cfg, nil\n}\n\n// Validate validates the configuration\nfunc (c *CombinedConfig) Validate() error {\n\tif c.Server.ExposedAddress == \"\" {\n\t\treturn fmt.Errorf(\"server.exposedAddress is required\")\n\t}\n\tif c.Server.DataDir == \"\" {\n\t\treturn fmt.Errorf(\"server.dataDir is required\")\n\t}\n\n\t// Validate STUN ports\n\tseen := make(map[int]bool)\n\tfor _, port := range c.Server.StunPorts {\n\t\tif port <= 0 || port > 65535 {\n\t\t\treturn fmt.Errorf(\"invalid server.stunPorts value %d: must be between 1 and 65535\", port)\n\t\t}\n\t\tif seen[port] {\n\t\t\treturn fmt.Errorf(\"duplicate STUN port %d in server.stunPorts\", port)\n\t\t}\n\t\tseen[port] = true\n\t}\n\n\t// authSecret is required only if running local relay (no external relay configured)\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\tif !hasExternalRelay && c.Server.AuthSecret == \"\" {\n\t\treturn fmt.Errorf(\"server.authSecret is required when running local relay\")\n\t}\n\n\treturn nil\n}\n\n// HasTLSCert returns true if TLS certificate files are configured\nfunc (c *CombinedConfig) HasTLSCert() bool {\n\treturn c.Server.TLS.CertFile != \"\" && c.Server.TLS.KeyFile != \"\"\n}\n\n// HasLetsEncrypt returns true if Let's Encrypt is configured\nfunc (c *CombinedConfig) HasLetsEncrypt() bool {\n\treturn c.Server.TLS.LetsEncrypt.Enabled &&\n\t\tc.Server.TLS.LetsEncrypt.DataDir != \"\" &&\n\t\tlen(c.Server.TLS.LetsEncrypt.Domains) > 0\n}\n\n// parseExplicitProtocol parses an explicit protocol string to nbconfig.Protocol\nfunc parseExplicitProtocol(proto string) (nbconfig.Protocol, bool) {\n\tswitch strings.ToLower(proto) {\n\tcase \"udp\":\n\t\treturn nbconfig.UDP, true\n\tcase \"dtls\":\n\t\treturn nbconfig.DTLS, true\n\tcase \"tcp\":\n\t\treturn nbconfig.TCP, true\n\tcase \"http\":\n\t\treturn nbconfig.HTTP, true\n\tcase \"https\":\n\t\treturn nbconfig.HTTPS, true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\n// parseStunProtocol determines protocol for STUN/TURN servers.\n// stun: → UDP, stuns: → DTLS, turn: → UDP, turns: → DTLS\n// Explicit proto overrides URI scheme. Defaults to UDP.\nfunc parseStunProtocol(uri, proto string) nbconfig.Protocol {\n\tif proto != \"\" {\n\t\tif p, ok := parseExplicitProtocol(proto); ok {\n\t\t\treturn p\n\t\t}\n\t}\n\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"stuns:\"):\n\t\treturn nbconfig.DTLS\n\tcase strings.HasPrefix(uri, \"turns:\"):\n\t\treturn nbconfig.DTLS\n\tdefault:\n\t\t// stun:, turn:, or no scheme - default to UDP\n\t\treturn nbconfig.UDP\n\t}\n}\n\n// parseSignalProtocol determines protocol for Signal servers.\n// https:// → HTTPS, http:// → HTTP. Defaults to HTTPS.\nfunc parseSignalProtocol(uri string) nbconfig.Protocol {\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"http://\"):\n\t\treturn nbconfig.HTTP\n\tdefault:\n\t\t// https:// or no scheme - default to HTTPS\n\t\treturn nbconfig.HTTPS\n\t}\n}\n\n// stripSignalProtocol removes the protocol prefix from a signal URI.\n// Returns just the host:port (e.g., \"selfhosted2.demo.netbird.io:443\").\nfunc stripSignalProtocol(uri string) string {\n\turi = strings.TrimPrefix(uri, \"https://\")\n\turi = strings.TrimPrefix(uri, \"http://\")\n\treturn uri\n}\n\nfunc buildRelayConfig(relays RelaysConfig) (*nbconfig.Relay, error) {\n\tvar ttl time.Duration\n\tif relays.CredentialsTTL != \"\" {\n\t\tvar err error\n\t\tttl, err = time.ParseDuration(relays.CredentialsTTL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid relay credentials TTL %q: %w\", relays.CredentialsTTL, err)\n\t\t}\n\t}\n\treturn &nbconfig.Relay{\n\t\tAddresses: relays.Addresses,\n\t\tCredentialsTTL: util.Duration{Duration: ttl},\n\t\tSecret: relays.Secret,\n\t}, nil\n}\n\n// buildEmbeddedIdPConfig builds the embedded IdP configuration.\n// authStore overrides auth.storage when set.\nfunc (c *CombinedConfig) buildEmbeddedIdPConfig(mgmt ManagementConfig) (*idp.EmbeddedIdPConfig, error) {\n\tauthStorageType := mgmt.Auth.Storage.Type\n\tauthStorageDSN := c.Server.AuthStore.DSN\n\tif c.Server.AuthStore.Engine != \"\" {\n\t\tauthStorageType = c.Server.AuthStore.Engine\n\t}\n\tif authStorageType == \"\" {\n\t\tauthStorageType = \"sqlite3\"\n\t}\n\tauthStorageFile := \"\"\n\tif authStorageType == \"postgres\" {\n\t\tif authStorageDSN == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"authStore.dsn is required when authStore.engine is postgres\")\n\t\t}\n\t} else {\n\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n\t}\n\n\tcfg := &idp.EmbeddedIdPConfig{\n\t\tEnabled: true,\n\t\tIssuer: mgmt.Auth.Issuer,\n\t\tLocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,\n\t\tSignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,\n\t\tStorage: idp.EmbeddedStorageConfig{\n\t\t\tType: authStorageType,\n\t\t\tConfig: idp.EmbeddedStorageTypeConfig{\n\t\t\t\tFile: authStorageFile,\n\t\t\t\tDSN: authStorageDSN,\n\t\t\t},\n\t\t},\n\t\tDashboardRedirectURIs: mgmt.Auth.DashboardRedirectURIs,\n\t\tCLIRedirectURIs: mgmt.Auth.CLIRedirectURIs,\n\t}\n\n\tif mgmt.Auth.Owner != nil && mgmt.Auth.Owner.Email != \"\" {\n\t\tcfg.Owner = &idp.OwnerConfig{\n\t\t\tEmail: mgmt.Auth.Owner.Email,\n\t\t\tHash: mgmt.Auth.Owner.Password,\n\t\t}\n\t}\n\n\treturn cfg, nil\n}\n\n// ToManagementConfig converts CombinedConfig to management server config\nfunc (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {\n\tmgmt := c.Management\n\n\t// Build STUN hosts\n\tvar stuns []*nbconfig.Host\n\tfor _, s := range mgmt.Stuns {\n\t\tstuns = append(stuns, &nbconfig.Host{\n\t\t\tURI: s.URI,\n\t\t\tProto: parseStunProtocol(s.URI, s.Proto),\n\t\t\tUsername: s.Username,\n\t\t\tPassword: s.Password,\n\t\t})\n\t}\n\n\t// Build relay config\n\tvar relayConfig *nbconfig.Relay\n\tif len(mgmt.Relays.Addresses) > 0 || mgmt.Relays.Secret != \"\" {\n\t\trelay, err := buildRelayConfig(mgmt.Relays)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trelayConfig = relay\n\t}\n\n\t// Build signal config\n\tvar signalConfig *nbconfig.Host\n\tif mgmt.SignalURI != \"\" {\n\t\tsignalConfig = &nbconfig.Host{\n\t\t\tURI: stripSignalProtocol(mgmt.SignalURI),\n\t\t\tProto: parseSignalProtocol(mgmt.SignalURI),\n\t\t}\n\t}\n\n\t// Build store config\n\tstoreConfig := nbconfig.StoreConfig{\n\t\tEngine: types.Engine(mgmt.Store.Engine),\n\t}\n\n\t// Build reverse proxy config\n\treverseProxy := nbconfig.ReverseProxy{\n\t\tTrustedHTTPProxiesCount: mgmt.ReverseProxy.TrustedHTTPProxiesCount,\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedHTTPProxies {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedHTTPProxies = append(reverseProxy.TrustedHTTPProxies, prefix)\n\t\t}\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedPeers {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedPeers = append(reverseProxy.TrustedPeers, prefix)\n\t\t}\n\t}\n\n\t// Build HTTP config (required, even if empty)\n\thttpConfig := &nbconfig.HttpServerConfig{}\n\n\t// Build embedded IDP config (always enabled in combined server)\n\tembeddedIdP, err := c.buildEmbeddedIdPConfig(mgmt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Set HTTP config fields for embedded IDP\n\thttpConfig.AuthIssuer = mgmt.Auth.Issuer\n\thttpConfig.AuthAudience = \"netbird-dashboard\"\n\thttpConfig.AuthClientID = httpConfig.AuthAudience\n\thttpConfig.CLIAuthAudience = \"netbird-cli\"\n\thttpConfig.AuthUserIDClaim = \"sub\"\n\thttpConfig.AuthKeysLocation = mgmt.Auth.Issuer + \"/keys\"\n\thttpConfig.OIDCConfigEndpoint = mgmt.Auth.Issuer + \"/.well-known/openid-configuration\"\n\thttpConfig.IdpSignKeyRefreshEnabled = mgmt.Auth.SignKeyRefreshEnabled\n\tcallbackURL := strings.TrimSuffix(httpConfig.AuthIssuer, \"/oauth2\")\n\thttpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull\n\n\treturn &nbconfig.Config{\n\t\tStuns: stuns,\n\t\tRelay: relayConfig,\n\t\tSignal: signalConfig,\n\t\tDatadir: mgmt.DataDir,\n\t\tDataStoreEncryptionKey: mgmt.Store.EncryptionKey,\n\t\tHttpConfig: httpConfig,\n\t\tStoreConfig: storeConfig,\n\t\tReverseProxy: reverseProxy,\n\t\tDisableDefaultPolicy: mgmt.DisableDefaultPolicy,\n\t\tEmbeddedIdP: embeddedIdP,\n\t}, nil\n}\n\n// ApplyEmbeddedIdPConfig applies embedded IdP configuration to the management config.\n// This mirrors the logic in management/cmd/management.go ApplyEmbeddedIdPConfig.\nfunc ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config, mgmtPort int, disableSingleAccMode bool) error {\n\tif cfg.EmbeddedIdP == nil || !cfg.EmbeddedIdP.Enabled {\n\t\treturn nil\n\t}\n\n\t// Embedded IdP requires single account mode\n\tif disableSingleAccMode {\n\t\treturn fmt.Errorf(\"embedded IdP requires single account mode; multiple account mode is not supported with embedded IdP\")\n\t}\n\n\t// Set LocalAddress for embedded IdP, used for internal JWT validation\n\tcfg.EmbeddedIdP.LocalAddress = fmt.Sprintf(\"localhost:%d\", mgmtPort)\n\n\t// Set storage defaults based on Datadir\n\tif cfg.EmbeddedIdP.Storage.Type == \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Type = \"sqlite3\"\n\t}\n\tif cfg.EmbeddedIdP.Storage.Config.File == \"\" && cfg.Datadir != \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")\n\t}\n\n\tissuer := cfg.EmbeddedIdP.Issuer\n\n\t// Ensure HttpConfig exists\n\tif cfg.HttpConfig == nil {\n\t\tcfg.HttpConfig = &nbconfig.HttpServerConfig{}\n\t}\n\n\t// Set HttpConfig values from EmbeddedIdP\n\tcfg.HttpConfig.AuthIssuer = issuer\n\tcfg.HttpConfig.AuthAudience = \"netbird-dashboard\"\n\tcfg.HttpConfig.CLIAuthAudience = \"netbird-cli\"\n\tcfg.HttpConfig.AuthUserIDClaim = \"sub\"\n\tcfg.HttpConfig.AuthKeysLocation = issuer + \"/keys\"\n\tcfg.HttpConfig.OIDCConfigEndpoint = issuer + \"/.well-known/openid-configuration\"\n\tcfg.HttpConfig.IdpSignKeyRefreshEnabled = true\n\n\treturn nil\n}\n\n// EnsureEncryptionKey generates an encryption key if not set.\n// Unlike management server, we don't write back to the config file.\nfunc EnsureEncryptionKey(ctx context.Context, cfg *nbconfig.Config) error {\n\tif cfg.DataStoreEncryptionKey != \"\" {\n\t\treturn nil\n\t}\n\n\tlog.WithContext(ctx).Infof(\"DataStoreEncryptionKey is not set, generating a new key\")\n\tkey, err := crypt.GenerateKey()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate datastore encryption key: %v\", err)\n\t}\n\tcfg.DataStoreEncryptionKey = key\n\tkeyPreview := key[:8] + \"...\"\n\tlog.WithContext(ctx).Warnf(\"DataStoreEncryptionKey generated (%s); add it to your config file under 'server.store.encryptionKey' to persist across restarts\", keyPreview)\n\n\treturn nil\n}\n\n// LogConfigInfo logs informational messages about the loaded configuration\nfunc LogConfigInfo(cfg *nbconfig.Config) {\n\tif cfg.EmbeddedIdP != nil && cfg.EmbeddedIdP.Enabled {\n\t\tlog.Infof(\"running with the embedded IdP: %v\", cfg.EmbeddedIdP.Issuer)\n\t}\n\tif cfg.Relay != nil {\n\t\tlog.Infof(\"Relay addresses: %v\", cfg.Relay.Addresses)\n\t}\n}","newString":"package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/netbirdio/netbird/management/server/idp\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n\n\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n)\n\n// CombinedConfig is the root configuration for the combined server.\n// The combined server is primarily a Management server with optional embedded\n// Signal, Relay, and STUN services.\n//\n// Architecture:\n// - Management: Always runs locally (this IS the management server)\n// - Signal: Runs locally by default; disabled if server.signalUri is set\n// - Relay: Runs locally by default; disabled if server.relays is set\n// - STUN: Runs locally on port 3478 by default; disabled if server.stuns is set\n//\n// All user-facing settings are under \"server\". The relay/signal/management\n// fields are internal and populated automatically from server settings.\ntype CombinedConfig struct {\n\tServer ServerConfig `yaml:\"server\"`\n\n\t// Internal configs - populated from Server settings, not user-configurable\n\tRelay RelayConfig `yaml:\"-\"`\n\tSignal SignalConfig `yaml:\"-\"`\n\tManagement ManagementConfig `yaml:\"-\"`\n}\n\n// ServerConfig contains server-wide settings\n// In simplified mode, this contains all configuration\ntype ServerConfig struct {\n\tListenAddress string `yaml:\"listenAddress\"`\n\tMetricsPort int `yaml:\"metricsPort\"`\n\tHealthcheckAddress string `yaml:\"healthcheckAddress\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tLogFile string `yaml:\"logFile\"`\n\tTLS TLSConfig `yaml:\"tls\"`\n\n\t// Simplified config fields (used when relay/signal/management sections are omitted)\n\tExposedAddress string `yaml:\"exposedAddress\"` // Public address with protocol (e.g., \"https://example.com:443\")\n\tStunPorts []int `yaml:\"stunPorts\"` // STUN ports (empty to disable local STUN)\n\tAuthSecret string `yaml:\"authSecret\"` // Shared secret for relay authentication\n\tDataDir string `yaml:\"dataDir\"` // Data directory for all services\n\n\t// External service overrides (simplified mode)\n\t// When these are set, the corresponding local service is NOT started\n\t// and these values are used for client configuration instead\n\tStuns []HostConfig `yaml:\"stuns\"` // External STUN servers (disables local STUN)\n\tRelays RelaysConfig `yaml:\"relays\"` // External relay servers (disables local relay)\n\tSignalURI string `yaml:\"signalUri\"` // External signal server (disables local signal)\n\n\t// Management settings (simplified mode)\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tActivityStore StoreConfig `yaml:\"activityStore\"`\n\tAuthStore StoreConfig `yaml:\"authStore\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// TLSConfig contains TLS/HTTPS settings\ntype TLSConfig struct {\n\tCertFile string `yaml:\"certFile\"`\n\tKeyFile string `yaml:\"keyFile\"`\n\tLetsEncrypt LetsEncryptConfig `yaml:\"letsencrypt\"`\n}\n\n// LetsEncryptConfig contains Let's Encrypt settings\ntype LetsEncryptConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDomains []string `yaml:\"domains\"`\n\tEmail string `yaml:\"email\"`\n\tAWSRoute53 bool `yaml:\"awsRoute53\"`\n}\n\n// RelayConfig contains relay service settings\ntype RelayConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tExposedAddress string `yaml:\"exposedAddress\"`\n\tAuthSecret string `yaml:\"authSecret\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tStun StunConfig `yaml:\"stun\"`\n}\n\n// StunConfig contains embedded STUN service settings\ntype StunConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tPorts []int `yaml:\"ports\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// SignalConfig contains signal service settings\ntype SignalConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// ManagementConfig contains management service settings\ntype ManagementConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDnsDomain string `yaml:\"dnsDomain\"`\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tDisableDefaultPolicy bool `yaml:\"disableDefaultPolicy\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStuns []HostConfig `yaml:\"stuns\"`\n\tRelays RelaysConfig `yaml:\"relays\"`\n\tSignalURI string `yaml:\"signalUri\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// AuthConfig contains authentication/identity provider settings\ntype AuthConfig struct {\n\tIssuer string `yaml:\"issuer\"`\n\tLocalAuthDisabled bool `yaml:\"localAuthDisabled\"`\n\tSignKeyRefreshEnabled bool `yaml:\"signKeyRefreshEnabled\"`\n\tStorage AuthStorageConfig `yaml:\"storage\"`\n\tDashboardRedirectURIs []string `yaml:\"dashboardRedirectURIs\"`\n\tCLIRedirectURIs []string `yaml:\"cliRedirectURIs\"`\n\tOwner *AuthOwnerConfig `yaml:\"owner,omitempty\"`\n}\n\n// AuthStorageConfig contains auth storage settings\ntype AuthStorageConfig struct {\n\tType string `yaml:\"type\"`\n\tFile string `yaml:\"file\"`\n}\n\n// AuthOwnerConfig contains initial admin user settings\ntype AuthOwnerConfig struct {\n\tEmail string `yaml:\"email\"`\n\tPassword string `yaml:\"password\"`\n}\n\n// HostConfig represents a STUN/TURN/Signal host\ntype HostConfig struct {\n\tURI string `yaml:\"uri\"`\n\tProto string `yaml:\"proto,omitempty\"` // udp, dtls, tcp, http, https - defaults based on URI scheme\n\tUsername string `yaml:\"username,omitempty\"`\n\tPassword string `yaml:\"password,omitempty\"`\n}\n\n// RelaysConfig contains external relay server settings for clients\ntype RelaysConfig struct {\n\tAddresses []string `yaml:\"addresses\"`\n\tCredentialsTTL string `yaml:\"credentialsTTL\"`\n\tSecret string `yaml:\"secret\"`\n}\n\n// StoreConfig contains database settings\ntype StoreConfig struct {\n\tEngine string `yaml:\"engine\"`\n\tEncryptionKey string `yaml:\"encryptionKey\"`\n\tDSN string `yaml:\"dsn\"` // Connection string for postgres or mysql engines\n\tFile string `yaml:\"file\"` // SQLite database file path (optional, defaults to dataDir)\n}\n\n// ReverseProxyConfig contains reverse proxy settings\ntype ReverseProxyConfig struct {\n\tTrustedHTTPProxies []string `yaml:\"trustedHTTPProxies\"`\n\tTrustedHTTPProxiesCount uint `yaml:\"trustedHTTPProxiesCount\"`\n\tTrustedPeers []string `yaml:\"trustedPeers\"`\n}\n\n// DefaultConfig returns a CombinedConfig with default values\nfunc DefaultConfig() *CombinedConfig {\n\treturn &CombinedConfig{\n\t\tServer: ServerConfig{\n\t\t\tListenAddress: \":443\",\n\t\t\tMetricsPort: 9090,\n\t\t\tHealthcheckAddress: \":9000\",\n\t\t\tLogLevel: \"info\",\n\t\t\tLogFile: \"console\",\n\t\t\tStunPorts: []int{3478},\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t\tRelay: RelayConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\tStun: StunConfig{\n\t\t\t\tEnabled: false,\n\t\t\t\tPorts: []int{3478},\n\t\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\t},\n\t\t},\n\t\tSignal: SignalConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t},\n\t\tManagement: ManagementConfig{\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRelays: RelaysConfig{\n\t\t\t\tCredentialsTTL: \"12h\",\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t}\n}\n\n// hasRequiredSettings returns true if the configuration has the required server settings\nfunc (c *CombinedConfig) hasRequiredSettings() bool {\n\treturn c.Server.ExposedAddress != \"\"\n}\n\n// parseExposedAddress extracts protocol, host, and host:port from the exposed address\n// Input format: \"https://example.com:443\" or \"http://example.com:8080\" or \"example.com:443\"\n// Returns: protocol (\"https\" or \"http\"), hostname only, and host:port\nfunc parseExposedAddress(exposedAddress string) (protocol, hostname, hostPort string) {\n\t// Default to https if no protocol specified\n\tprotocol = \"https\"\n\thostPort = exposedAddress\n\n\t// Check for protocol prefix\n\tif strings.HasPrefix(exposedAddress, \"https://\") {\n\t\tprotocol = \"https\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"https://\")\n\t} else if strings.HasPrefix(exposedAddress, \"http://\") {\n\t\tprotocol = \"http\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"http://\")\n\t}\n\n\t// Extract hostname (without port)\n\thostname = hostPort\n\tif host, _, err := net.SplitHostPort(hostPort); err == nil {\n\t\thostname = host\n\t}\n\n\treturn protocol, hostname, hostPort\n}\n\n// ApplySimplifiedDefaults populates internal relay/signal/management configs from server settings.\n// Management is always enabled. Signal, Relay, and STUN are enabled unless external\n// overrides are configured (server.signalUri, server.relays, server.stuns).\nfunc (c *CombinedConfig) ApplySimplifiedDefaults() {\n\tif !c.hasRequiredSettings() {\n\t\treturn\n\t}\n\n\t// Parse exposed address to extract protocol and hostname\n\texposedProto, exposedHost, exposedHostPort := parseExposedAddress(c.Server.ExposedAddress)\n\n\t// Check for external service overrides\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\thasExternalSignal := c.Server.SignalURI != \"\"\n\thasExternalStuns := len(c.Server.Stuns) > 0\n\n\t// Default stunPorts to [3478] if not specified and no external STUN\n\tif len(c.Server.StunPorts) == 0 && !hasExternalStuns {\n\t\tc.Server.StunPorts = []int{3478}\n\t}\n\n\tc.applyRelayDefaults(exposedProto, exposedHostPort, hasExternalRelay, hasExternalStuns)\n\tc.applySignalDefaults(hasExternalSignal)\n\tc.applyManagementDefaults(exposedHost)\n\n\t// Auto-configure client settings (stuns, relays, signalUri)\n\tc.autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort, hasExternalStuns, hasExternalRelay, hasExternalSignal)\n}\n\n// applyRelayDefaults configures the relay service if no external relay is configured.\nfunc (c *CombinedConfig) applyRelayDefaults(exposedProto, exposedHostPort string, hasExternalRelay, hasExternalStuns bool) {\n\tif hasExternalRelay {\n\t\treturn\n\t}\n\n\tc.Relay.Enabled = true\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\tc.Relay.ExposedAddress = fmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort)\n\tc.Relay.AuthSecret = c.Server.AuthSecret\n\tif c.Relay.LogLevel == \"\" {\n\t\tc.Relay.LogLevel = c.Server.LogLevel\n\t}\n\n\t// Enable local STUN only if no external STUN servers and stunPorts are configured\n\tif !hasExternalStuns && len(c.Server.StunPorts) > 0 {\n\t\tc.Relay.Stun.Enabled = true\n\t\tc.Relay.Stun.Ports = c.Server.StunPorts\n\t\tif c.Relay.Stun.LogLevel == \"\" {\n\t\t\tc.Relay.Stun.LogLevel = c.Server.LogLevel\n\t\t}\n\t}\n}\n\n// applySignalDefaults configures the signal service if no external signal is configured.\nfunc (c *CombinedConfig) applySignalDefaults(hasExternalSignal bool) {\n\tif hasExternalSignal {\n\t\treturn\n\t}\n\n\tc.Signal.Enabled = true\n\tif c.Signal.LogLevel == \"\" {\n\t\tc.Signal.LogLevel = c.Server.LogLevel\n\t}\n}\n\n// applyManagementDefaults configures the management service (always enabled).\nfunc (c *CombinedConfig) applyManagementDefaults(exposedHost string) {\n\tc.Management.Enabled = true\n\tif c.Management.LogLevel == \"\" {\n\t\tc.Management.LogLevel = c.Server.LogLevel\n\t}\n\tif c.Management.DataDir == \"\" || c.Management.DataDir == \"/var/lib/netbird/\" {\n\t\tc.Management.DataDir = c.Server.DataDir\n\t}\n\tc.Management.DnsDomain = exposedHost\n\tc.Management.DisableAnonymousMetrics = c.Server.DisableAnonymousMetrics\n\tc.Management.DisableGeoliteUpdate = c.Server.DisableGeoliteUpdate\n\t// Copy auth config from server if management auth issuer is not set\n\tif c.Management.Auth.Issuer == \"\" && c.Server.Auth.Issuer != \"\" {\n\t\tc.Management.Auth = c.Server.Auth\n\t}\n\n\t// Copy store config from server if not set\n\tif c.Management.Store.Engine == \"\" || c.Management.Store.Engine == \"sqlite\" {\n\t\tif c.Server.Store.Engine != \"\" {\n\t\t\tc.Management.Store = c.Server.Store\n\t\t}\n\t}\n\n\t// Copy reverse proxy config from server\n\tif len(c.Server.ReverseProxy.TrustedHTTPProxies) > 0 || c.Server.ReverseProxy.TrustedHTTPProxiesCount > 0 || len(c.Server.ReverseProxy.TrustedPeers) > 0 {\n\t\tc.Management.ReverseProxy = c.Server.ReverseProxy\n\t}\n}\n\n// autoConfigureClientSettings sets up STUN/relay/signal URIs for clients\n// External overrides from server config take precedence over auto-generated values\nfunc (c *CombinedConfig) autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort string, hasExternalStuns, hasExternalRelay, hasExternalSignal bool) {\n\t// Determine relay protocol from exposed protocol\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\n\t// Configure STUN servers for clients\n\tif hasExternalStuns {\n\t\t// Use external STUN servers from server config\n\t\tc.Management.Stuns = c.Server.Stuns\n\t} else if len(c.Server.StunPorts) > 0 && len(c.Management.Stuns) == 0 {\n\t\t// Auto-configure local STUN servers for all ports\n\t\tfor _, port := range c.Server.StunPorts {\n\t\t\tc.Management.Stuns = append(c.Management.Stuns, HostConfig{\n\t\t\t\tURI: fmt.Sprintf(\"stun:%s:%d\", exposedHost, port),\n\t\t\t})\n\t\t}\n\t}\n\n\t// Configure relay for clients\n\tif hasExternalRelay {\n\t\t// Use external relay config from server\n\t\tc.Management.Relays = c.Server.Relays\n\t} else if len(c.Management.Relays.Addresses) == 0 {\n\t\t// Auto-configure local relay\n\t\tc.Management.Relays.Addresses = []string{\n\t\t\tfmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort),\n\t\t}\n\t}\n\tif c.Management.Relays.Secret == \"\" {\n\t\tc.Management.Relays.Secret = c.Server.AuthSecret\n\t}\n\tif c.Management.Relays.CredentialsTTL == \"\" {\n\t\tc.Management.Relays.CredentialsTTL = \"12h\"\n\t}\n\n\t// Configure signal for clients\n\tif hasExternalSignal {\n\t\t// Use external signal URI from server config\n\t\tc.Management.SignalURI = c.Server.SignalURI\n\t} else if c.Management.SignalURI == \"\" {\n\t\t// Auto-configure local signal\n\t\tc.Management.SignalURI = fmt.Sprintf(\"%s://%s\", exposedProto, exposedHostPort)\n\t}\n}\n\n// LoadConfig loads configuration from a YAML file\nfunc LoadConfig(configPath string) (*CombinedConfig, error) {\n\tcfg := DefaultConfig()\n\n\tif configPath == \"\" {\n\t\treturn cfg, nil\n\t}\n\n\tdata, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read config file: %w\", err)\n\t}\n\n\tif err := yaml.Unmarshal(data, cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse config file: %w\", err)\n\t}\n\n\t// Populate internal configs from server settings\n\tcfg.ApplySimplifiedDefaults()\n\n\treturn cfg, nil\n}\n\n// Validate validates the configuration\nfunc (c *CombinedConfig) Validate() error {\n\tif c.Server.ExposedAddress == \"\" {\n\t\treturn fmt.Errorf(\"server.exposedAddress is required\")\n\t}\n\tif c.Server.DataDir == \"\" {\n\t\treturn fmt.Errorf(\"server.dataDir is required\")\n\t}\n\n\t// Validate STUN ports\n\tseen := make(map[int]bool)\n\tfor _, port := range c.Server.StunPorts {\n\t\tif port <= 0 || port > 65535 {\n\t\t\treturn fmt.Errorf(\"invalid server.stunPorts value %d: must be between 1 and 65535\", port)\n\t\t}\n\t\tif seen[port] {\n\t\t\treturn fmt.Errorf(\"duplicate STUN port %d in server.stunPorts\", port)\n\t\t}\n\t\tseen[port] = true\n\t}\n\n\t// authSecret is required only if running local relay (no external relay configured)\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\tif !hasExternalRelay && c.Server.AuthSecret == \"\" {\n\t\treturn fmt.Errorf(\"server.authSecret is required when running local relay\")\n\t}\n\n\treturn nil\n}\n\n// HasTLSCert returns true if TLS certificate files are configured\nfunc (c *CombinedConfig) HasTLSCert() bool {\n\treturn c.Server.TLS.CertFile != \"\" && c.Server.TLS.KeyFile != \"\"\n}\n\n// HasLetsEncrypt returns true if Let's Encrypt is configured\nfunc (c *CombinedConfig) HasLetsEncrypt() bool {\n\treturn c.Server.TLS.LetsEncrypt.Enabled &&\n\t\tc.Server.TLS.LetsEncrypt.DataDir != \"\" &&\n\t\tlen(c.Server.TLS.LetsEncrypt.Domains) > 0\n}\n\n// parseExplicitProtocol parses an explicit protocol string to nbconfig.Protocol\nfunc parseExplicitProtocol(proto string) (nbconfig.Protocol, bool) {\n\tswitch strings.ToLower(proto) {\n\tcase \"udp\":\n\t\treturn nbconfig.UDP, true\n\tcase \"dtls\":\n\t\treturn nbconfig.DTLS, true\n\tcase \"tcp\":\n\t\treturn nbconfig.TCP, true\n\tcase \"http\":\n\t\treturn nbconfig.HTTP, true\n\tcase \"https\":\n\t\treturn nbconfig.HTTPS, true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\n// parseStunProtocol determines protocol for STUN/TURN servers.\n// stun: → UDP, stuns: → DTLS, turn: → UDP, turns: → DTLS\n// Explicit proto overrides URI scheme. Defaults to UDP.\nfunc parseStunProtocol(uri, proto string) nbconfig.Protocol {\n\tif proto != \"\" {\n\t\tif p, ok := parseExplicitProtocol(proto); ok {\n\t\t\treturn p\n\t\t}\n\t}\n\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"stuns:\"):\n\t\treturn nbconfig.DTLS\n\tcase strings.HasPrefix(uri, \"turns:\"):\n\t\treturn nbconfig.DTLS\n\tdefault:\n\t\t// stun:, turn:, or no scheme - default to UDP\n\t\treturn nbconfig.UDP\n\t}\n}\n\n// parseSignalProtocol determines protocol for Signal servers.\n// https:// → HTTPS, http:// → HTTP. Defaults to HTTPS.\nfunc parseSignalProtocol(uri string) nbconfig.Protocol {\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"http://\"):\n\t\treturn nbconfig.HTTP\n\tdefault:\n\t\t// https:// or no scheme - default to HTTPS\n\t\treturn nbconfig.HTTPS\n\t}\n}\n\n// stripSignalProtocol removes the protocol prefix from a signal URI.\n// Returns just the host:port (e.g., \"selfhosted2.demo.netbird.io:443\").\nfunc stripSignalProtocol(uri string) string {\n\turi = strings.TrimPrefix(uri, \"https://\")\n\turi = strings.TrimPrefix(uri, \"http://\")\n\treturn uri\n}\n\nfunc buildRelayConfig(relays RelaysConfig) (*nbconfig.Relay, error) {\n\tvar ttl time.Duration\n\tif relays.CredentialsTTL != \"\" {\n\t\tvar err error\n\t\tttl, err = time.ParseDuration(relays.CredentialsTTL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid relay credentials TTL %q: %w\", relays.CredentialsTTL, err)\n\t\t}\n\t}\n\treturn &nbconfig.Relay{\n\t\tAddresses: relays.Addresses,\n\t\tCredentialsTTL: util.Duration{Duration: ttl},\n\t\tSecret: relays.Secret,\n\t}, nil\n}\n\n// buildEmbeddedIdPConfig builds the embedded IdP configuration.\n// authStore overrides auth.storage when set.\nfunc (c *CombinedConfig) buildEmbeddedIdPConfig(mgmt ManagementConfig) (*idp.EmbeddedIdPConfig, error) {\n\tauthStorageType := mgmt.Auth.Storage.Type\n\tauthStorageDSN := c.Server.AuthStore.DSN\n\tif c.Server.AuthStore.Engine != \"\" {\n\t\tauthStorageType = c.Server.AuthStore.Engine\n\t}\n\tif authStorageType == \"\" {\n\t\tauthStorageType = \"sqlite3\"\n\t}\n\tauthStorageFile := \"\"\n\tif authStorageType == \"postgres\" {\n\t\tif authStorageDSN == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"authStore.dsn is required when authStore.engine is postgres\")\n\t\t}\n\t} else {\n\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n\t\tif c.Server.AuthStore.File != \"\" {\n\t\t\tauthStorageFile = c.Server.AuthStore.File\n\t\t}\n\t}\n\n\tcfg := &idp.EmbeddedIdPConfig{\n\t\tEnabled: true,\n\t\tIssuer: mgmt.Auth.Issuer,\n\t\tLocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,\n\t\tSignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,\n\t\tStorage: idp.EmbeddedStorageConfig{\n\t\t\tType: authStorageType,\n\t\t\tConfig: idp.EmbeddedStorageTypeConfig{\n\t\t\t\tFile: authStorageFile,\n\t\t\t\tDSN: authStorageDSN,\n\t\t\t},\n\t\t},\n\t\tDashboardRedirectURIs: mgmt.Auth.DashboardRedirectURIs,\n\t\tCLIRedirectURIs: mgmt.Auth.CLIRedirectURIs,\n\t}\n\n\tif mgmt.Auth.Owner != nil && mgmt.Auth.Owner.Email != \"\" {\n\t\tcfg.Owner = &idp.OwnerConfig{\n\t\t\tEmail: mgmt.Auth.Owner.Email,\n\t\t\tHash: mgmt.Auth.Owner.Password,\n\t\t}\n\t}\n\n\treturn cfg, nil\n}\n\n// ToManagementConfig converts CombinedConfig to management server config\nfunc (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {\n\tmgmt := c.Management\n\n\t// Build STUN hosts\n\tvar stuns []*nbconfig.Host\n\tfor _, s := range mgmt.Stuns {\n\t\tstuns = append(stuns, &nbconfig.Host{\n\t\t\tURI: s.URI,\n\t\t\tProto: parseStunProtocol(s.URI, s.Proto),\n\t\t\tUsername: s.Username,\n\t\t\tPassword: s.Password,\n\t\t})\n\t}\n\n\t// Build relay config\n\tvar relayConfig *nbconfig.Relay\n\tif len(mgmt.Relays.Addresses) > 0 || mgmt.Relays.Secret != \"\" {\n\t\trelay, err := buildRelayConfig(mgmt.Relays)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trelayConfig = relay\n\t}\n\n\t// Build signal config\n\tvar signalConfig *nbconfig.Host\n\tif mgmt.SignalURI != \"\" {\n\t\tsignalConfig = &nbconfig.Host{\n\t\t\tURI: stripSignalProtocol(mgmt.SignalURI),\n\t\t\tProto: parseSignalProtocol(mgmt.SignalURI),\n\t\t}\n\t}\n\n\t// Build store config\n\tstoreConfig := nbconfig.StoreConfig{\n\t\tEngine: types.Engine(mgmt.Store.Engine),\n\t}\n\n\t// Build reverse proxy config\n\treverseProxy := nbconfig.ReverseProxy{\n\t\tTrustedHTTPProxiesCount: mgmt.ReverseProxy.TrustedHTTPProxiesCount,\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedHTTPProxies {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedHTTPProxies = append(reverseProxy.TrustedHTTPProxies, prefix)\n\t\t}\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedPeers {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedPeers = append(reverseProxy.TrustedPeers, prefix)\n\t\t}\n\t}\n\n\t// Build HTTP config (required, even if empty)\n\thttpConfig := &nbconfig.HttpServerConfig{}\n\n\t// Build embedded IDP config (always enabled in combined server)\n\tembeddedIdP, err := c.buildEmbeddedIdPConfig(mgmt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Set HTTP config fields for embedded IDP\n\thttpConfig.AuthIssuer = mgmt.Auth.Issuer\n\thttpConfig.AuthAudience = \"netbird-dashboard\"\n\thttpConfig.AuthClientID = httpConfig.AuthAudience\n\thttpConfig.CLIAuthAudience = \"netbird-cli\"\n\thttpConfig.AuthUserIDClaim = \"sub\"\n\thttpConfig.AuthKeysLocation = mgmt.Auth.Issuer + \"/keys\"\n\thttpConfig.OIDCConfigEndpoint = mgmt.Auth.Issuer + \"/.well-known/openid-configuration\"\n\thttpConfig.IdpSignKeyRefreshEnabled = mgmt.Auth.SignKeyRefreshEnabled\n\tcallbackURL := strings.TrimSuffix(httpConfig.AuthIssuer, \"/oauth2\")\n\thttpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull\n\n\treturn &nbconfig.Config{\n\t\tStuns: stuns,\n\t\tRelay: relayConfig,\n\t\tSignal: signalConfig,\n\t\tDatadir: mgmt.DataDir,\n\t\tDataStoreEncryptionKey: mgmt.Store.EncryptionKey,\n\t\tHttpConfig: httpConfig,\n\t\tStoreConfig: storeConfig,\n\t\tReverseProxy: reverseProxy,\n\t\tDisableDefaultPolicy: mgmt.DisableDefaultPolicy,\n\t\tEmbeddedIdP: embeddedIdP,\n\t}, nil\n}\n\n// ApplyEmbeddedIdPConfig applies embedded IdP configuration to the management config.\n// This mirrors the logic in management/cmd/management.go ApplyEmbeddedIdPConfig.\nfunc ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config, mgmtPort int, disableSingleAccMode bool) error {\n\tif cfg.EmbeddedIdP == nil || !cfg.EmbeddedIdP.Enabled {\n\t\treturn nil\n\t}\n\n\t// Embedded IdP requires single account mode\n\tif disableSingleAccMode {\n\t\treturn fmt.Errorf(\"embedded IdP requires single account mode; multiple account mode is not supported with embedded IdP\")\n\t}\n\n\t// Set LocalAddress for embedded IdP, used for internal JWT validation\n\tcfg.EmbeddedIdP.LocalAddress = fmt.Sprintf(\"localhost:%d\", mgmtPort)\n\n\t// Set storage defaults based on Datadir\n\tif cfg.EmbeddedIdP.Storage.Type == \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Type = \"sqlite3\"\n\t}\n\tif cfg.EmbeddedIdP.Storage.Config.File == \"\" && cfg.Datadir != \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")\n\t}\n\n\tissuer := cfg.EmbeddedIdP.Issuer\n\n\t// Ensure HttpConfig exists\n\tif cfg.HttpConfig == nil {\n\t\tcfg.HttpConfig = &nbconfig.HttpServerConfig{}\n\t}\n\n\t// Set HttpConfig values from EmbeddedIdP\n\tcfg.HttpConfig.AuthIssuer = issuer\n\tcfg.HttpConfig.AuthAudience = \"netbird-dashboard\"\n\tcfg.HttpConfig.CLIAuthAudience = \"netbird-cli\"\n\tcfg.HttpConfig.AuthUserIDClaim = \"sub\"\n\tcfg.HttpConfig.AuthKeysLocation = issuer + \"/keys\"\n\tcfg.HttpConfig.OIDCConfigEndpoint = issuer + \"/.well-known/openid-configuration\"\n\tcfg.HttpConfig.IdpSignKeyRefreshEnabled = true\n\n\treturn nil\n}\n\n// EnsureEncryptionKey generates an encryption key if not set.\n// Unlike management server, we don't write back to the config file.\nfunc EnsureEncryptionKey(ctx context.Context, cfg *nbconfig.Config) error {\n\tif cfg.DataStoreEncryptionKey != \"\" {\n\t\treturn nil\n\t}\n\n\tlog.WithContext(ctx).Infof(\"DataStoreEncryptionKey is not set, generating a new key\")\n\tkey, err := crypt.GenerateKey()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate datastore encryption key: %v\", err)\n\t}\n\tcfg.DataStoreEncryptionKey = key\n\tkeyPreview := key[:8] + \"...\"\n\tlog.WithContext(ctx).Warnf(\"DataStoreEncryptionKey generated (%s); add it to your config file under 'server.store.encryptionKey' to persist across restarts\", keyPreview)\n\n\treturn nil\n}\n\n// LogConfigInfo logs informational messages about the loaded configuration\nfunc LogConfigInfo(cfg *nbconfig.Config) {\n\tif cfg.EmbeddedIdP != nil && cfg.EmbeddedIdP.Enabled {\n\t\tlog.Infof(\"running with the embedded IdP: %v\", cfg.EmbeddedIdP.Issuer)\n\t}\n\tif cfg.Relay != nil {\n\t\tlog.Infof(\"Relay addresses: %v\", cfg.Relay.Addresses)\n\t}\n}","originalFile":"package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/netbirdio/netbird/management/server/idp\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n\n\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n)\n\n// CombinedConfig is the root configuration for the combined server.\n// The combined server is primarily a Management server with optional embedded\n// Signal, Relay, and STUN services.\n//\n// Architecture:\n// - Management: Always runs locally (this IS the management server)\n// - Signal: Runs locally by default; disabled if server.signalUri is set\n// - Relay: Runs locally by default; disabled if server.relays is set\n// - STUN: Runs locally on port 3478 by default; disabled if server.stuns is set\n//\n// All user-facing settings are under \"server\". The relay/signal/management\n// fields are internal and populated automatically from server settings.\ntype CombinedConfig struct {\n\tServer ServerConfig `yaml:\"server\"`\n\n\t// Internal configs - populated from Server settings, not user-configurable\n\tRelay RelayConfig `yaml:\"-\"`\n\tSignal SignalConfig `yaml:\"-\"`\n\tManagement ManagementConfig `yaml:\"-\"`\n}\n\n// ServerConfig contains server-wide settings\n// In simplified mode, this contains all configuration\ntype ServerConfig struct {\n\tListenAddress string `yaml:\"listenAddress\"`\n\tMetricsPort int `yaml:\"metricsPort\"`\n\tHealthcheckAddress string `yaml:\"healthcheckAddress\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tLogFile string `yaml:\"logFile\"`\n\tTLS TLSConfig `yaml:\"tls\"`\n\n\t// Simplified config fields (used when relay/signal/management sections are omitted)\n\tExposedAddress string `yaml:\"exposedAddress\"` // Public address with protocol (e.g., \"https://example.com:443\")\n\tStunPorts []int `yaml:\"stunPorts\"` // STUN ports (empty to disable local STUN)\n\tAuthSecret string `yaml:\"authSecret\"` // Shared secret for relay authentication\n\tDataDir string `yaml:\"dataDir\"` // Data directory for all services\n\n\t// External service overrides (simplified mode)\n\t// When these are set, the corresponding local service is NOT started\n\t// and these values are used for client configuration instead\n\tStuns []HostConfig `yaml:\"stuns\"` // External STUN servers (disables local STUN)\n\tRelays RelaysConfig `yaml:\"relays\"` // External relay servers (disables local relay)\n\tSignalURI string `yaml:\"signalUri\"` // External signal server (disables local signal)\n\n\t// Management settings (simplified mode)\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tActivityStore StoreConfig `yaml:\"activityStore\"`\n\tAuthStore StoreConfig `yaml:\"authStore\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// TLSConfig contains TLS/HTTPS settings\ntype TLSConfig struct {\n\tCertFile string `yaml:\"certFile\"`\n\tKeyFile string `yaml:\"keyFile\"`\n\tLetsEncrypt LetsEncryptConfig `yaml:\"letsencrypt\"`\n}\n\n// LetsEncryptConfig contains Let's Encrypt settings\ntype LetsEncryptConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDomains []string `yaml:\"domains\"`\n\tEmail string `yaml:\"email\"`\n\tAWSRoute53 bool `yaml:\"awsRoute53\"`\n}\n\n// RelayConfig contains relay service settings\ntype RelayConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tExposedAddress string `yaml:\"exposedAddress\"`\n\tAuthSecret string `yaml:\"authSecret\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tStun StunConfig `yaml:\"stun\"`\n}\n\n// StunConfig contains embedded STUN service settings\ntype StunConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tPorts []int `yaml:\"ports\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// SignalConfig contains signal service settings\ntype SignalConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// ManagementConfig contains management service settings\ntype ManagementConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDnsDomain string `yaml:\"dnsDomain\"`\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tDisableDefaultPolicy bool `yaml:\"disableDefaultPolicy\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStuns []HostConfig `yaml:\"stuns\"`\n\tRelays RelaysConfig `yaml:\"relays\"`\n\tSignalURI string `yaml:\"signalUri\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// AuthConfig contains authentication/identity provider settings\ntype AuthConfig struct {\n\tIssuer string `yaml:\"issuer\"`\n\tLocalAuthDisabled bool `yaml:\"localAuthDisabled\"`\n\tSignKeyRefreshEnabled bool `yaml:\"signKeyRefreshEnabled\"`\n\tStorage AuthStorageConfig `yaml:\"storage\"`\n\tDashboardRedirectURIs []string `yaml:\"dashboardRedirectURIs\"`\n\tCLIRedirectURIs []string `yaml:\"cliRedirectURIs\"`\n\tOwner *AuthOwnerConfig `yaml:\"owner,omitempty\"`\n}\n\n// AuthStorageConfig contains auth storage settings\ntype AuthStorageConfig struct {\n\tType string `yaml:\"type\"`\n\tFile string `yaml:\"file\"`\n}\n\n// AuthOwnerConfig contains initial admin user settings\ntype AuthOwnerConfig struct {\n\tEmail string `yaml:\"email\"`\n\tPassword string `yaml:\"password\"`\n}\n\n// HostConfig represents a STUN/TURN/Signal host\ntype HostConfig struct {\n\tURI string `yaml:\"uri\"`\n\tProto string `yaml:\"proto,omitempty\"` // udp, dtls, tcp, http, https - defaults based on URI scheme\n\tUsername string `yaml:\"username,omitempty\"`\n\tPassword string `yaml:\"password,omitempty\"`\n}\n\n// RelaysConfig contains external relay server settings for clients\ntype RelaysConfig struct {\n\tAddresses []string `yaml:\"addresses\"`\n\tCredentialsTTL string `yaml:\"credentialsTTL\"`\n\tSecret string `yaml:\"secret\"`\n}\n\n// StoreConfig contains database settings\ntype StoreConfig struct {\n\tEngine string `yaml:\"engine\"`\n\tEncryptionKey string `yaml:\"encryptionKey\"`\n\tDSN string `yaml:\"dsn\"` // Connection string for postgres or mysql engines\n\tFile string `yaml:\"file\"` // SQLite database file path (optional, defaults to dataDir)\n}\n\n// ReverseProxyConfig contains reverse proxy settings\ntype ReverseProxyConfig struct {\n\tTrustedHTTPProxies []string `yaml:\"trustedHTTPProxies\"`\n\tTrustedHTTPProxiesCount uint `yaml:\"trustedHTTPProxiesCount\"`\n\tTrustedPeers []string `yaml:\"trustedPeers\"`\n}\n\n// DefaultConfig returns a CombinedConfig with default values\nfunc DefaultConfig() *CombinedConfig {\n\treturn &CombinedConfig{\n\t\tServer: ServerConfig{\n\t\t\tListenAddress: \":443\",\n\t\t\tMetricsPort: 9090,\n\t\t\tHealthcheckAddress: \":9000\",\n\t\t\tLogLevel: \"info\",\n\t\t\tLogFile: \"console\",\n\t\t\tStunPorts: []int{3478},\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t\tRelay: RelayConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\tStun: StunConfig{\n\t\t\t\tEnabled: false,\n\t\t\t\tPorts: []int{3478},\n\t\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\t},\n\t\t},\n\t\tSignal: SignalConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t},\n\t\tManagement: ManagementConfig{\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRelays: RelaysConfig{\n\t\t\t\tCredentialsTTL: \"12h\",\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t}\n}\n\n// hasRequiredSettings returns true if the configuration has the required server settings\nfunc (c *CombinedConfig) hasRequiredSettings() bool {\n\treturn c.Server.ExposedAddress != \"\"\n}\n\n// parseExposedAddress extracts protocol, host, and host:port from the exposed address\n// Input format: \"https://example.com:443\" or \"http://example.com:8080\" or \"example.com:443\"\n// Returns: protocol (\"https\" or \"http\"), hostname only, and host:port\nfunc parseExposedAddress(exposedAddress string) (protocol, hostname, hostPort string) {\n\t// Default to https if no protocol specified\n\tprotocol = \"https\"\n\thostPort = exposedAddress\n\n\t// Check for protocol prefix\n\tif strings.HasPrefix(exposedAddress, \"https://\") {\n\t\tprotocol = \"https\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"https://\")\n\t} else if strings.HasPrefix(exposedAddress, \"http://\") {\n\t\tprotocol = \"http\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"http://\")\n\t}\n\n\t// Extract hostname (without port)\n\thostname = hostPort\n\tif host, _, err := net.SplitHostPort(hostPort); err == nil {\n\t\thostname = host\n\t}\n\n\treturn protocol, hostname, hostPort\n}\n\n// ApplySimplifiedDefaults populates internal relay/signal/management configs from server settings.\n// Management is always enabled. Signal, Relay, and STUN are enabled unless external\n// overrides are configured (server.signalUri, server.relays, server.stuns).\nfunc (c *CombinedConfig) ApplySimplifiedDefaults() {\n\tif !c.hasRequiredSettings() {\n\t\treturn\n\t}\n\n\t// Parse exposed address to extract protocol and hostname\n\texposedProto, exposedHost, exposedHostPort := parseExposedAddress(c.Server.ExposedAddress)\n\n\t// Check for external service overrides\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\thasExternalSignal := c.Server.SignalURI != \"\"\n\thasExternalStuns := len(c.Server.Stuns) > 0\n\n\t// Default stunPorts to [3478] if not specified and no external STUN\n\tif len(c.Server.StunPorts) == 0 && !hasExternalStuns {\n\t\tc.Server.StunPorts = []int{3478}\n\t}\n\n\tc.applyRelayDefaults(exposedProto, exposedHostPort, hasExternalRelay, hasExternalStuns)\n\tc.applySignalDefaults(hasExternalSignal)\n\tc.applyManagementDefaults(exposedHost)\n\n\t// Auto-configure client settings (stuns, relays, signalUri)\n\tc.autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort, hasExternalStuns, hasExternalRelay, hasExternalSignal)\n}\n\n// applyRelayDefaults configures the relay service if no external relay is configured.\nfunc (c *CombinedConfig) applyRelayDefaults(exposedProto, exposedHostPort string, hasExternalRelay, hasExternalStuns bool) {\n\tif hasExternalRelay {\n\t\treturn\n\t}\n\n\tc.Relay.Enabled = true\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\tc.Relay.ExposedAddress = fmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort)\n\tc.Relay.AuthSecret = c.Server.AuthSecret\n\tif c.Relay.LogLevel == \"\" {\n\t\tc.Relay.LogLevel = c.Server.LogLevel\n\t}\n\n\t// Enable local STUN only if no external STUN servers and stunPorts are configured\n\tif !hasExternalStuns && len(c.Server.StunPorts) > 0 {\n\t\tc.Relay.Stun.Enabled = true\n\t\tc.Relay.Stun.Ports = c.Server.StunPorts\n\t\tif c.Relay.Stun.LogLevel == \"\" {\n\t\t\tc.Relay.Stun.LogLevel = c.Server.LogLevel\n\t\t}\n\t}\n}\n\n// applySignalDefaults configures the signal service if no external signal is configured.\nfunc (c *CombinedConfig) applySignalDefaults(hasExternalSignal bool) {\n\tif hasExternalSignal {\n\t\treturn\n\t}\n\n\tc.Signal.Enabled = true\n\tif c.Signal.LogLevel == \"\" {\n\t\tc.Signal.LogLevel = c.Server.LogLevel\n\t}\n}\n\n// applyManagementDefaults configures the management service (always enabled).\nfunc (c *CombinedConfig) applyManagementDefaults(exposedHost string) {\n\tc.Management.Enabled = true\n\tif c.Management.LogLevel == \"\" {\n\t\tc.Management.LogLevel = c.Server.LogLevel\n\t}\n\tif c.Management.DataDir == \"\" || c.Management.DataDir == \"/var/lib/netbird/\" {\n\t\tc.Management.DataDir = c.Server.DataDir\n\t}\n\tc.Management.DnsDomain = exposedHost\n\tc.Management.DisableAnonymousMetrics = c.Server.DisableAnonymousMetrics\n\tc.Management.DisableGeoliteUpdate = c.Server.DisableGeoliteUpdate\n\t// Copy auth config from server if management auth issuer is not set\n\tif c.Management.Auth.Issuer == \"\" && c.Server.Auth.Issuer != \"\" {\n\t\tc.Management.Auth = c.Server.Auth\n\t}\n\n\t// Copy store config from server if not set\n\tif c.Management.Store.Engine == \"\" || c.Management.Store.Engine == \"sqlite\" {\n\t\tif c.Server.Store.Engine != \"\" {\n\t\t\tc.Management.Store = c.Server.Store\n\t\t}\n\t}\n\n\t// Copy reverse proxy config from server\n\tif len(c.Server.ReverseProxy.TrustedHTTPProxies) > 0 || c.Server.ReverseProxy.TrustedHTTPProxiesCount > 0 || len(c.Server.ReverseProxy.TrustedPeers) > 0 {\n\t\tc.Management.ReverseProxy = c.Server.ReverseProxy\n\t}\n}\n\n// autoConfigureClientSettings sets up STUN/relay/signal URIs for clients\n// External overrides from server config take precedence over auto-generated values\nfunc (c *CombinedConfig) autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort string, hasExternalStuns, hasExternalRelay, hasExternalSignal bool) {\n\t// Determine relay protocol from exposed protocol\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\n\t// Configure STUN servers for clients\n\tif hasExternalStuns {\n\t\t// Use external STUN servers from server config\n\t\tc.Management.Stuns = c.Server.Stuns\n\t} else if len(c.Server.StunPorts) > 0 && len(c.Management.Stuns) == 0 {\n\t\t// Auto-configure local STUN servers for all ports\n\t\tfor _, port := range c.Server.StunPorts {\n\t\t\tc.Management.Stuns = append(c.Management.Stuns, HostConfig{\n\t\t\t\tURI: fmt.Sprintf(\"stun:%s:%d\", exposedHost, port),\n\t\t\t})\n\t\t}\n\t}\n\n\t// Configure relay for clients\n\tif hasExternalRelay {\n\t\t// Use external relay config from server\n\t\tc.Management.Relays = c.Server.Relays\n\t} else if len(c.Management.Relays.Addresses) == 0 {\n\t\t// Auto-configure local relay\n\t\tc.Management.Relays.Addresses = []string{\n\t\t\tfmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort),\n\t\t}\n\t}\n\tif c.Management.Relays.Secret == \"\" {\n\t\tc.Management.Relays.Secret = c.Server.AuthSecret\n\t}\n\tif c.Management.Relays.CredentialsTTL == \"\" {\n\t\tc.Management.Relays.CredentialsTTL = \"12h\"\n\t}\n\n\t// Configure signal for clients\n\tif hasExternalSignal {\n\t\t// Use external signal URI from server config\n\t\tc.Management.SignalURI = c.Server.SignalURI\n\t} else if c.Management.SignalURI == \"\" {\n\t\t// Auto-configure local signal\n\t\tc.Management.SignalURI = fmt.Sprintf(\"%s://%s\", exposedProto, exposedHostPort)\n\t}\n}\n\n// LoadConfig loads configuration from a YAML file\nfunc LoadConfig(configPath string) (*CombinedConfig, error) {\n\tcfg := DefaultConfig()\n\n\tif configPath == \"\" {\n\t\treturn cfg, nil\n\t}\n\n\tdata, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read config file: %w\", err)\n\t}\n\n\tif err := yaml.Unmarshal(data, cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse config file: %w\", err)\n\t}\n\n\t// Populate internal configs from server settings\n\tcfg.ApplySimplifiedDefaults()\n\n\treturn cfg, nil\n}\n\n// Validate validates the configuration\nfunc (c *CombinedConfig) Validate() error {\n\tif c.Server.ExposedAddress == \"\" {\n\t\treturn fmt.Errorf(\"server.exposedAddress is required\")\n\t}\n\tif c.Server.DataDir == \"\" {\n\t\treturn fmt.Errorf(\"server.dataDir is required\")\n\t}\n\n\t// Validate STUN ports\n\tseen := make(map[int]bool)\n\tfor _, port := range c.Server.StunPorts {\n\t\tif port <= 0 || port > 65535 {\n\t\t\treturn fmt.Errorf(\"invalid server.stunPorts value %d: must be between 1 and 65535\", port)\n\t\t}\n\t\tif seen[port] {\n\t\t\treturn fmt.Errorf(\"duplicate STUN port %d in server.stunPorts\", port)\n\t\t}\n\t\tseen[port] = true\n\t}\n\n\t// authSecret is required only if running local relay (no external relay configured)\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\tif !hasExternalRelay && c.Server.AuthSecret == \"\" {\n\t\treturn fmt.Errorf(\"server.authSecret is required when running local relay\")\n\t}\n\n\treturn nil\n}\n\n// HasTLSCert returns true if TLS certificate files are configured\nfunc (c *CombinedConfig) HasTLSCert() bool {\n\treturn c.Server.TLS.CertFile != \"\" && c.Server.TLS.KeyFile != \"\"\n}\n\n// HasLetsEncrypt returns true if Let's Encrypt is configured\nfunc (c *CombinedConfig) HasLetsEncrypt() bool {\n\treturn c.Server.TLS.LetsEncrypt.Enabled &&\n\t\tc.Server.TLS.LetsEncrypt.DataDir != \"\" &&\n\t\tlen(c.Server.TLS.LetsEncrypt.Domains) > 0\n}\n\n// parseExplicitProtocol parses an explicit protocol string to nbconfig.Protocol\nfunc parseExplicitProtocol(proto string) (nbconfig.Protocol, bool) {\n\tswitch strings.ToLower(proto) {\n\tcase \"udp\":\n\t\treturn nbconfig.UDP, true\n\tcase \"dtls\":\n\t\treturn nbconfig.DTLS, true\n\tcase \"tcp\":\n\t\treturn nbconfig.TCP, true\n\tcase \"http\":\n\t\treturn nbconfig.HTTP, true\n\tcase \"https\":\n\t\treturn nbconfig.HTTPS, true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\n// parseStunProtocol determines protocol for STUN/TURN servers.\n// stun: → UDP, stuns: → DTLS, turn: → UDP, turns: → DTLS\n// Explicit proto overrides URI scheme. Defaults to UDP.\nfunc parseStunProtocol(uri, proto string) nbconfig.Protocol {\n\tif proto != \"\" {\n\t\tif p, ok := parseExplicitProtocol(proto); ok {\n\t\t\treturn p\n\t\t}\n\t}\n\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"stuns:\"):\n\t\treturn nbconfig.DTLS\n\tcase strings.HasPrefix(uri, \"turns:\"):\n\t\treturn nbconfig.DTLS\n\tdefault:\n\t\t// stun:, turn:, or no scheme - default to UDP\n\t\treturn nbconfig.UDP\n\t}\n}\n\n// parseSignalProtocol determines protocol for Signal servers.\n// https:// → HTTPS, http:// → HTTP. Defaults to HTTPS.\nfunc parseSignalProtocol(uri string) nbconfig.Protocol {\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"http://\"):\n\t\treturn nbconfig.HTTP\n\tdefault:\n\t\t// https:// or no scheme - default to HTTPS\n\t\treturn nbconfig.HTTPS\n\t}\n}\n\n// stripSignalProtocol removes the protocol prefix from a signal URI.\n// Returns just the host:port (e.g., \"selfhosted2.demo.netbird.io:443\").\nfunc stripSignalProtocol(uri string) string {\n\turi = strings.TrimPrefix(uri, \"https://\")\n\turi = strings.TrimPrefix(uri, \"http://\")\n\treturn uri\n}\n\nfunc buildRelayConfig(relays RelaysConfig) (*nbconfig.Relay, error) {\n\tvar ttl time.Duration\n\tif relays.CredentialsTTL != \"\" {\n\t\tvar err error\n\t\tttl, err = time.ParseDuration(relays.CredentialsTTL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid relay credentials TTL %q: %w\", relays.CredentialsTTL, err)\n\t\t}\n\t}\n\treturn &nbconfig.Relay{\n\t\tAddresses: relays.Addresses,\n\t\tCredentialsTTL: util.Duration{Duration: ttl},\n\t\tSecret: relays.Secret,\n\t}, nil\n}\n\n// buildEmbeddedIdPConfig builds the embedded IdP configuration.\n// authStore overrides auth.storage when set.\nfunc (c *CombinedConfig) buildEmbeddedIdPConfig(mgmt ManagementConfig) (*idp.EmbeddedIdPConfig, error) {\n\tauthStorageType := mgmt.Auth.Storage.Type\n\tauthStorageDSN := c.Server.AuthStore.DSN\n\tif c.Server.AuthStore.Engine != \"\" {\n\t\tauthStorageType = c.Server.AuthStore.Engine\n\t}\n\tif authStorageType == \"\" {\n\t\tauthStorageType = \"sqlite3\"\n\t}\n\tauthStorageFile := \"\"\n\tif authStorageType == \"postgres\" {\n\t\tif authStorageDSN == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"authStore.dsn is required when authStore.engine is postgres\")\n\t\t}\n\t} else {\n\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n\t}\n\n\tcfg := &idp.EmbeddedIdPConfig{\n\t\tEnabled: true,\n\t\tIssuer: mgmt.Auth.Issuer,\n\t\tLocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,\n\t\tSignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,\n\t\tStorage: idp.EmbeddedStorageConfig{\n\t\t\tType: authStorageType,\n\t\t\tConfig: idp.EmbeddedStorageTypeConfig{\n\t\t\t\tFile: authStorageFile,\n\t\t\t\tDSN: authStorageDSN,\n\t\t\t},\n\t\t},\n\t\tDashboardRedirectURIs: mgmt.Auth.DashboardRedirectURIs,\n\t\tCLIRedirectURIs: mgmt.Auth.CLIRedirectURIs,\n\t}\n\n\tif mgmt.Auth.Owner != nil && mgmt.Auth.Owner.Email != \"\" {\n\t\tcfg.Owner = &idp.OwnerConfig{\n\t\t\tEmail: mgmt.Auth.Owner.Email,\n\t\t\tHash: mgmt.Auth.Owner.Password,\n\t\t}\n\t}\n\n\treturn cfg, nil\n}\n\n// ToManagementConfig converts CombinedConfig to management server config\nfunc (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {\n\tmgmt := c.Management\n\n\t// Build STUN hosts\n\tvar stuns []*nbconfig.Host\n\tfor _, s := range mgmt.Stuns {\n\t\tstuns = append(stuns, &nbconfig.Host{\n\t\t\tURI: s.URI,\n\t\t\tProto: parseStunProtocol(s.URI, s.Proto),\n\t\t\tUsername: s.Username,\n\t\t\tPassword: s.Password,\n\t\t})\n\t}\n\n\t// Build relay config\n\tvar relayConfig *nbconfig.Relay\n\tif len(mgmt.Relays.Addresses) > 0 || mgmt.Relays.Secret != \"\" {\n\t\trelay, err := buildRelayConfig(mgmt.Relays)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trelayConfig = relay\n\t}\n\n\t// Build signal config\n\tvar signalConfig *nbconfig.Host\n\tif mgmt.SignalURI != \"\" {\n\t\tsignalConfig = &nbconfig.Host{\n\t\t\tURI: stripSignalProtocol(mgmt.SignalURI),\n\t\t\tProto: parseSignalProtocol(mgmt.SignalURI),\n\t\t}\n\t}\n\n\t// Build store config\n\tstoreConfig := nbconfig.StoreConfig{\n\t\tEngine: types.Engine(mgmt.Store.Engine),\n\t}\n\n\t// Build reverse proxy config\n\treverseProxy := nbconfig.ReverseProxy{\n\t\tTrustedHTTPProxiesCount: mgmt.ReverseProxy.TrustedHTTPProxiesCount,\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedHTTPProxies {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedHTTPProxies = append(reverseProxy.TrustedHTTPProxies, prefix)\n\t\t}\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedPeers {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedPeers = append(reverseProxy.TrustedPeers, prefix)\n\t\t}\n\t}\n\n\t// Build HTTP config (required, even if empty)\n\thttpConfig := &nbconfig.HttpServerConfig{}\n\n\t// Build embedded IDP config (always enabled in combined server)\n\tembeddedIdP, err := c.buildEmbeddedIdPConfig(mgmt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Set HTTP config fields for embedded IDP\n\thttpConfig.AuthIssuer = mgmt.Auth.Issuer\n\thttpConfig.AuthAudience = \"netbird-dashboard\"\n\thttpConfig.AuthClientID = httpConfig.AuthAudience\n\thttpConfig.CLIAuthAudience = \"netbird-cli\"\n\thttpConfig.AuthUserIDClaim = \"sub\"\n\thttpConfig.AuthKeysLocation = mgmt.Auth.Issuer + \"/keys\"\n\thttpConfig.OIDCConfigEndpoint = mgmt.Auth.Issuer + \"/.well-known/openid-configuration\"\n\thttpConfig.IdpSignKeyRefreshEnabled = mgmt.Auth.SignKeyRefreshEnabled\n\tcallbackURL := strings.TrimSuffix(httpConfig.AuthIssuer, \"/oauth2\")\n\thttpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull\n\n\treturn &nbconfig.Config{\n\t\tStuns: stuns,\n\t\tRelay: relayConfig,\n\t\tSignal: signalConfig,\n\t\tDatadir: mgmt.DataDir,\n\t\tDataStoreEncryptionKey: mgmt.Store.EncryptionKey,\n\t\tHttpConfig: httpConfig,\n\t\tStoreConfig: storeConfig,\n\t\tReverseProxy: reverseProxy,\n\t\tDisableDefaultPolicy: mgmt.DisableDefaultPolicy,\n\t\tEmbeddedIdP: embeddedIdP,\n\t}, nil\n}\n\n// ApplyEmbeddedIdPConfig applies embedded IdP configuration to the management config.\n// This mirrors the logic in management/cmd/management.go ApplyEmbeddedIdPConfig.\nfunc ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config, mgmtPort int, disableSingleAccMode bool) error {\n\tif cfg.EmbeddedIdP == nil || !cfg.EmbeddedIdP.Enabled {\n\t\treturn nil\n\t}\n\n\t// Embedded IdP requires single account mode\n\tif disableSingleAccMode {\n\t\treturn fmt.Errorf(\"embedded IdP requires single account mode; multiple account mode is not supported with embedded IdP\")\n\t}\n\n\t// Set LocalAddress for embedded IdP, used for internal JWT validation\n\tcfg.EmbeddedIdP.LocalAddress = fmt.Sprintf(\"localhost:%d\", mgmtPort)\n\n\t// Set storage defaults based on Datadir\n\tif cfg.EmbeddedIdP.Storage.Type == \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Type = \"sqlite3\"\n\t}\n\tif cfg.EmbeddedIdP.Storage.Config.File == \"\" && cfg.Datadir != \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")\n\t}\n\n\tissuer := cfg.EmbeddedIdP.Issuer\n\n\t// Ensure HttpConfig exists\n\tif cfg.HttpConfig == nil {\n\t\tcfg.HttpConfig = &nbconfig.HttpServerConfig{}\n\t}\n\n\t// Set HttpConfig values from EmbeddedIdP\n\tcfg.HttpConfig.AuthIssuer = issuer\n\tcfg.HttpConfig.AuthAudience = \"netbird-dashboard\"\n\tcfg.HttpConfig.CLIAuthAudience = \"netbird-cli\"\n\tcfg.HttpConfig.AuthUserIDClaim = \"sub\"\n\tcfg.HttpConfig.AuthKeysLocation = issuer + \"/keys\"\n\tcfg.HttpConfig.OIDCConfigEndpoint = issuer + \"/.well-known/openid-configuration\"\n\tcfg.HttpConfig.IdpSignKeyRefreshEnabled = true\n\n\treturn nil\n}\n\n// EnsureEncryptionKey generates an encryption key if not set.\n// Unlike management server, we don't write back to the config file.\nfunc EnsureEncryptionKey(ctx context.Context, cfg *nbconfig.Config) error {\n\tif cfg.DataStoreEncryptionKey != \"\" {\n\t\treturn nil\n\t}\n\n\tlog.WithContext(ctx).Infof(\"DataStoreEncryptionKey is not set, generating a new key\")\n\tkey, err := crypt.GenerateKey()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate datastore encryption key: %v\", err)\n\t}\n\tcfg.DataStoreEncryptionKey = key\n\tkeyPreview := key[:8] + \"...\"\n\tlog.WithContext(ctx).Warnf(\"DataStoreEncryptionKey generated (%s); add it to your config file under 'server.store.encryptionKey' to persist across restarts\", keyPreview)\n\n\treturn nil\n}\n\n// LogConfigInfo logs informational messages about the loaded configuration\nfunc LogConfigInfo(cfg *nbconfig.Config) {\n\tif cfg.EmbeddedIdP != nil && cfg.EmbeddedIdP.Enabled {\n\t\tlog.Infof(\"running with the embedded IdP: %v\", cfg.EmbeddedIdP.Issuer)\n\t}\n\tif cfg.Relay != nil {\n\t\tlog.Infof(\"Relay addresses: %v\", cfg.Relay.Addresses)\n\t}\n}\n","structuredPatch":[{"oldStart":569,"oldLines":6,"newStart":569,"newLines":9,"lines":[" }"," } else {"," authStorageFile = path.Join(mgmt.DataDir, \"idp.db\")","+ if c.Server.AuthStore.File != \"\" {","+ authStorageFile = c.Server.AuthStore.File","+ }"," }"," "," cfg := &idp.EmbeddedIdPConfig{"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"86b41705-73f9-4703-b63c-8ec2a36b1c3a"} +{"parentUuid":"7a02d6d6-16b4-4eb1-8e57-d3e55036f01b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01NeBeFKPioDimSbJdBhVvbU","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01BjKDVHwAg8qeddJ8yK7vSo","name":"TaskUpdate","input":{"taskId":"1","status":"completed"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":354,"cache_read_input_tokens":73344,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":354},"output_tokens":25,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYenJZX2jXV84ebcd5uUc","type":"assistant","uuid":"a438519e-32ae-43ad-b42c-61f115189aab","timestamp":"2026-03-02T19:10:18.268Z"} +{"parentUuid":"a438519e-32ae-43ad-b42c-61f115189aab","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01BjKDVHwAg8qeddJ8yK7vSo","type":"tool_result","content":"Updated task #1 status"}]},"uuid":"935aefca-1cdd-4ce1-aa04-fe927d075bf4","timestamp":"2026-03-02T19:10:18.309Z","toolUseResult":{"success":true,"taskId":"1","updatedFields":["status"],"statusChange":{"from":"in_progress","to":"completed"}},"sourceToolAssistantUUID":"a438519e-32ae-43ad-b42c-61f115189aab"} +{"parentUuid":"935aefca-1cdd-4ce1-aa04-fe927d075bf4","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_014v2hG1j348Fz2FFpyutnXo","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01S5jE8oPUxhiHwCqQrS4swp","name":"TaskUpdate","input":{"taskId":"2","status":"in_progress"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":88,"cache_read_input_tokens":73698,"output_tokens":72,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":88,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYenJnVdjpuevmqv3XCmT","type":"assistant","uuid":"61676374-a3be-438a-8936-f8795ea65fbe","timestamp":"2026-03-02T19:10:21.415Z"} +{"parentUuid":"61676374-a3be-438a-8936-f8795ea65fbe","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01S5jE8oPUxhiHwCqQrS4swp","type":"tool_result","content":"Updated task #2 status"}]},"uuid":"7f5690a7-d135-48a1-b491-9b30077dd28d","timestamp":"2026-03-02T19:10:21.446Z","toolUseResult":{"success":true,"taskId":"2","updatedFields":["status"],"statusChange":{"from":"pending","to":"in_progress"}},"sourceToolAssistantUUID":"61676374-a3be-438a-8936-f8795ea65fbe"} +{"parentUuid":"7f5690a7-d135-48a1-b491-9b30077dd28d","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_014h95t87Sb5w8zn7xPNSVet","type":"message","role":"assistant","content":[{"type":"text","text":"Now add env var support to the main store's `NewSqliteStore`:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":90,"cache_read_input_tokens":73786,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":90},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYenK199YiSWhXNBVNMKy","type":"assistant","uuid":"4ac32634-1eb8-464a-bb30-8c7b077d4d4a","timestamp":"2026-03-02T19:10:25.430Z"} +{"parentUuid":"4ac32634-1eb8-464a-bb30-8c7b077d4d4a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_014h95t87Sb5w8zn7xPNSVet","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01LaWremNhgk5uFRwPU5rwJm","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go","old_string":"// NewSqliteStore creates a new SQLite store.\nfunc NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tstoreStr := fmt.Sprintf(\"%s?cache=shared\", storeSqliteFileName)\n\tif runtime.GOOS == \"windows\" {\n\t\t// Vo avoid `The process cannot access the file because it is being used by another process` on Windows\n\t\tstoreStr = storeSqliteFileName\n\t}\n\n\tfile := filepath.Join(dataDir, storeStr)\n\tdb, err := gorm.Open(sqlite.Open(file), getGormConfig())","new_string":"// NewSqliteStore creates a new SQLite store.\nfunc NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tstoreFile := storeSqliteFileName\n\tif envFile, ok := os.LookupEnv(\"NB_STORE_ENGINE_SQLITE_FILE\"); ok && envFile != \"\" {\n\t\tstoreFile = envFile\n\t}\n\n\tstoreStr := fmt.Sprintf(\"%s?cache=shared\", storeFile)\n\tif runtime.GOOS == \"windows\" {\n\t\t// Vo avoid `The process cannot access the file because it is being used by another process` on Windows\n\t\tstoreStr = storeFile\n\t}\n\n\tvar file string\n\tif filepath.IsAbs(storeFile) {\n\t\tfile = storeStr\n\t} else {\n\t\tfile = filepath.Join(dataDir, storeStr)\n\t}\n\n\tdb, err := gorm.Open(sqlite.Open(file), getGormConfig())"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":90,"cache_read_input_tokens":73786,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":90},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYenK199YiSWhXNBVNMKy","type":"assistant","uuid":"fb087d62-f0f8-4cbc-beec-601fe80095ee","timestamp":"2026-03-02T19:10:33.349Z"} +{"parentUuid":"fb087d62-f0f8-4cbc-beec-601fe80095ee","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"type":"tool_result","content":"The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.","is_error":true,"tool_use_id":"toolu_01LaWremNhgk5uFRwPU5rwJm"}]},"uuid":"05af3018-d659-4f44-9132-c91553432ea1","timestamp":"2026-03-02T19:15:02.347Z","toolUseResult":"User rejected tool use","sourceToolAssistantUUID":"fb087d62-f0f8-4cbc-beec-601fe80095ee"} +{"parentUuid":"05af3018-d659-4f44-9132-c91553432ea1","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"type":"text","text":"[Request interrupted by user for tool use]"}]},"uuid":"6d949ab2-e9e3-40f1-ade8-6d37d9efb9b6","timestamp":"2026-03-02T19:15:02.351Z"} +{"type":"file-history-snapshot","messageId":"d4eea3a0-189a-4500-b784-8e496ce35a38","snapshot":{"messageId":"d4eea3a0-189a-4500-b784-8e496ce35a38","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v2","version":2,"backupTime":"2026-03-02T19:15:31.656Z"}},"timestamp":"2026-03-02T19:15:31.655Z"},"isSnapshotUpdate":false} +{"parentUuid":"6d949ab2-e9e3-40f1-ade8-6d37d9efb9b6","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":"we need to chevck if teh user provided query params to the file after ?"},"uuid":"d4eea3a0-189a-4500-b784-8e496ce35a38","timestamp":"2026-03-02T19:15:30.681Z","permissionMode":"default"} +{"parentUuid":"d4eea3a0-189a-4500-b784-8e496ce35a38","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01QrUnvAnesPWBzM6WKAsa3K","type":"message","role":"assistant","content":[{"type":"text","text":"\n\nGood point. If the user provides something like `store.db?cache=shared&mode=rwc`, we need to handle the `?` query params properly — both for the `filepath.IsAbs` check and for the `?cache=shared` default append logic.\n\nLet me rethink the approach:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":733,"cache_read_input_tokens":73876,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":733},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYenhqyP1LwfsrpCbRgGA","type":"assistant","uuid":"fa54a568-6b03-43d1-aa77-2760724893ba","timestamp":"2026-03-02T19:15:37.668Z"} +{"parentUuid":"fa54a568-6b03-43d1-aa77-2760724893ba","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01QrUnvAnesPWBzM6WKAsa3K","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01DkcodQE59wYY2kVhfw2K3i","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go","offset":2729,"limit":20},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":733,"cache_read_input_tokens":73876,"output_tokens":187,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":733,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYenhqyP1LwfsrpCbRgGA","type":"assistant","uuid":"6a9da530-f264-4e0e-abdb-3efb7476a7f5","timestamp":"2026-03-02T19:15:39.130Z"} +{"parentUuid":"6a9da530-f264-4e0e-abdb-3efb7476a7f5","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01DkcodQE59wYY2kVhfw2K3i","toolUseID":"toolu_01DkcodQE59wYY2kVhfw2K3i","timestamp":"2026-03-02T19:15:39.165Z","uuid":"61511938-4ac3-476a-9315-eaaaa1dd6ce8"} +{"parentUuid":"6a9da530-f264-4e0e-abdb-3efb7476a7f5","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01DkcodQE59wYY2kVhfw2K3i","type":"tool_result","content":" 2729→// NewSqliteStore creates a new SQLite store.\n 2730→func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n 2731→\tstoreStr := fmt.Sprintf(\"%s?cache=shared\", storeSqliteFileName)\n 2732→\tif runtime.GOOS == \"windows\" {\n 2733→\t\t// Vo avoid `The process cannot access the file because it is being used by another process` on Windows\n 2734→\t\tstoreStr = storeSqliteFileName\n 2735→\t}\n 2736→\n 2737→\tfile := filepath.Join(dataDir, storeStr)\n 2738→\tdb, err := gorm.Open(sqlite.Open(file), getGormConfig())\n 2739→\tif err != nil {\n 2740→\t\treturn nil, err\n 2741→\t}\n 2742→\n 2743→\treturn NewSqlStore(ctx, db, types.SqliteStoreEngine, metrics, skipMigration)\n 2744→}\n 2745→\n 2746→// NewPostgresqlStore creates a new Postgres store.\n 2747→func NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n 2748→\tdb, err := gorm.Open(postgres.Open(dsn), getGormConfig())"}]},"uuid":"82bc46ce-547f-49be-af4b-ad986ce500d7","timestamp":"2026-03-02T19:15:39.165Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go","content":"// NewSqliteStore creates a new SQLite store.\nfunc NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tstoreStr := fmt.Sprintf(\"%s?cache=shared\", storeSqliteFileName)\n\tif runtime.GOOS == \"windows\" {\n\t\t// Vo avoid `The process cannot access the file because it is being used by another process` on Windows\n\t\tstoreStr = storeSqliteFileName\n\t}\n\n\tfile := filepath.Join(dataDir, storeStr)\n\tdb, err := gorm.Open(sqlite.Open(file), getGormConfig())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn NewSqlStore(ctx, db, types.SqliteStoreEngine, metrics, skipMigration)\n}\n\n// NewPostgresqlStore creates a new Postgres store.\nfunc NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tdb, err := gorm.Open(postgres.Open(dsn), getGormConfig())","numLines":20,"startLine":2729,"totalLines":5278}},"sourceToolAssistantUUID":"6a9da530-f264-4e0e-abdb-3efb7476a7f5"} +{"parentUuid":"82bc46ce-547f-49be-af4b-ad986ce500d7","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01PUmZUxw2sbWPAUpcut9Hc7","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_014kGvpr6tksWe8eM22neEQ2","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go","offset":242,"limit":30},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":571,"cache_read_input_tokens":74609,"output_tokens":118,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":571,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYeniRTX7G2CaQWpc1Wdb","type":"assistant","uuid":"50e30e8e-dfc6-4473-8f96-7603841e0e63","timestamp":"2026-03-02T19:15:42.802Z"} +{"parentUuid":"50e30e8e-dfc6-4473-8f96-7603841e0e63","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_014kGvpr6tksWe8eM22neEQ2","toolUseID":"toolu_014kGvpr6tksWe8eM22neEQ2","timestamp":"2026-03-02T19:15:42.836Z","uuid":"ebbea09e-ca40-4c09-aa0a-f8d518d3393b"} +{"parentUuid":"50e30e8e-dfc6-4473-8f96-7603841e0e63","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_014kGvpr6tksWe8eM22neEQ2","type":"tool_result","content":" 242→func initDatabase(ctx context.Context, dataDir string) (*gorm.DB, error) {\n 243→\tvar dialector gorm.Dialector\n 244→\tvar storeEngine = types.SqliteStoreEngine\n 245→\n 246→\tif engine, ok := os.LookupEnv(storeEngineEnv); ok {\n 247→\t\tstoreEngine = types.Engine(engine)\n 248→\t}\n 249→\n 250→\tswitch storeEngine {\n 251→\tcase types.SqliteStoreEngine:\n 252→\t\tdialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB))\n 253→\tcase types.PostgresStoreEngine:\n 254→\t\tdsn, ok := os.LookupEnv(postgresDsnEnv)\n 255→\t\tif !ok {\n 256→\t\t\treturn nil, fmt.Errorf(\"%s environment variable not set\", postgresDsnEnv)\n 257→\t\t}\n 258→\t\tdialector = postgres.Open(dsn)\n 259→\tdefault:\n 260→\t\treturn nil, fmt.Errorf(\"unsupported store engine: %s\", storeEngine)\n 261→\t}\n 262→\tlog.WithContext(ctx).Infof(\"using %s as activity event store engine\", storeEngine)\n 263→\n 264→\tdb, err := gorm.Open(dialector, &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})\n 265→\tif err != nil {\n 266→\t\treturn nil, fmt.Errorf(\"open db connection: %w\", err)\n 267→\t}\n 268→\n 269→\treturn configureConnectionPool(db, storeEngine)\n 270→}\n 271→"}]},"uuid":"a7142ff9-39ee-46cb-a37d-ca9afba4c901","timestamp":"2026-03-02T19:15:42.836Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go","content":"func initDatabase(ctx context.Context, dataDir string) (*gorm.DB, error) {\n\tvar dialector gorm.Dialector\n\tvar storeEngine = types.SqliteStoreEngine\n\n\tif engine, ok := os.LookupEnv(storeEngineEnv); ok {\n\t\tstoreEngine = types.Engine(engine)\n\t}\n\n\tswitch storeEngine {\n\tcase types.SqliteStoreEngine:\n\t\tdialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB))\n\tcase types.PostgresStoreEngine:\n\t\tdsn, ok := os.LookupEnv(postgresDsnEnv)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"%s environment variable not set\", postgresDsnEnv)\n\t\t}\n\t\tdialector = postgres.Open(dsn)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported store engine: %s\", storeEngine)\n\t}\n\tlog.WithContext(ctx).Infof(\"using %s as activity event store engine\", storeEngine)\n\n\tdb, err := gorm.Open(dialector, &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open db connection: %w\", err)\n\t}\n\n\treturn configureConnectionPool(db, storeEngine)\n}\n","numLines":30,"startLine":242,"totalLines":296}},"sourceToolAssistantUUID":"50e30e8e-dfc6-4473-8f96-7603841e0e63"} +{"parentUuid":"a7142ff9-39ee-46cb-a37d-ca9afba4c901","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01KmocL94wUbWJ4RMi4dcu1d","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to handle the case where the user provides SQLite query params (e.g., `?cache=shared`, `?mode=rwc`) in the file path. The logic should be:\n\n1. If the user provides a file with `?` params (e.g., `/data/store.db?cache=shared`), use it as-is — don't append `?cache=shared` again\n2. If the user provides a file without `?` params (e.g., `/data/store.db`), append `?cache=shared` on non-Windows\n3. For `filepath.IsAbs`, we need to strip the `?` params before checking\n\n\n4. Now I'm implementing the function that splits the file path from query parameters and handles the conditional appending of cache settings based on whether params already exist.\n\nWriting the store initialization logic... Building the connection string...\n\nFor the activity store, I can simplify this since there's no default cache parameter to add. I just need to strip the query string to check if the path is absolute, then pass the full connection string (with any query params intact) directly to sqlite.Open. Now I'm handling the query parameter extraction and path resolution—splitting any query string from the file path, then constructing the connection string with the absolute path, and finally adding cache settings based on the OS (using `cache=shared` on non-Windows systems unless query params are already present). Writing the SQLite connection logic... I'm handling the SQLite file path resolution for the activity store, checking for environment overrides and then splitting the connection string to separate the file path from query parameters. The approach is to extract just the path component, join it with the data directory if it's relative, then reattach any query parameters that were in the original string.\n\nI should also make sure the `strings` import is included in the activity store file.\n\nNow I'm checking what imports are already there and considering whether to define the environment variable as a constant or just use the hardcoded string. Looking at the existing patterns in the codebase, the engine environment variable is already hardcoded as `\"NETBIRD_STORE_ENGINE\"`, so I'll stick with using the string directly for consistency rather than introducing a new constant.","signature":"Euo4CkYICxgCKkBzDDieXv55NSNADfGDPn6f6mNlVGFHcqPI2ZXLwKwzpKdgupCkfWqaa3CPeDMOK0bgChpmCh04Q934up7L38ZBEgyRGeAWIAViVEqgLUAaDBsGkE6e0zz2qQzCzyIwZ8uzqYIdpoPiIWQq49qizALCGOa4KxB/3EuzViyhBBEdMuJoPkZQGPv5HvXLz1ZqKtE3GdKADMx0A7uShj/VZyW/kDRDPq3P6whnvnj/eoy12lSHNLUo7ps6IVkHyzRLm5nP3GA1ehWZTuy1Utkr0YzKU/1Ng5GiVQvHKm14QwONYLKxOIJOvsm4QTsjdvDJOwI2ikyalLf1tIZBnwdfdsw6koRcslFx3jeo/v7pUm06o/JLlLIuhg1ap8epQk0TON+auHtjY2TWwHLGjEwbBzAu5n6ahe0VyY2yO0XDGKt8H3//d3TTUM7q1S23lNg37iR0Z0C7YtCZEBmKITU2lR47L1J42yvhahp3V1gqMJeQGfxmVMCh4Fm2JKXK2g8YXJk+K4EIm+gwo7zzpq2PYXK/FBD4u/j2VVfxOGoi3IPZbMBPVR33R910JCFojCkchd9+yY9kYR7+ek+yJYpx0tJQ5izQcmJLUnGV7O9Zu5y+Va8psRGMnT9YUO8B/0UA2pcWa2JiEg+IfQXMU7wnkFLP5JNrwDpYpAMq/gJc5QrmeBGFVwy6X0Cfu2PsbGdmk8zF6p/KlYp9X9yiOOK8PLZq+rqmG/84GDjOR+eBZkHEO4TtvWEnNQc0L/JfJq/w6CgnGnDOIpkbvZYqBwZLFOh8P+GaXTwhgui18hIUKwR++0MeSmmLOI6t9xxQyu+FgtCH3+e6WGe6jZ/HDvQAWe8HBC/wlfjZbZ29c5IBOcAhH3/4UmPGT2TNVL4VWP6ABA5+Dgrpb62I0ipqEb6/QLmb5YuK+5lCasjw0cmZZCJ4IPZD1QTcjVhdhV0TPC+PLSz/JQ7glDEmYMe0JC1irmPoCnsEwSg353vdfu2YA4Niqsl3onK1ae7MopnYsQF1TG4Mx2FTfmAFGXO6n9H0JyGuWwLOS6O90CxAxjwe1uk4mCbKGckkoHW4W1iJzB2GTya4gZdIlZ4NFBYNAGggU58KdVNPzb5IKT10/v8qnAP3nfo9RkjfGJfOItdMsTP96+OrkKy5+JG7wz1+giT1q8XU3x7ZRDo0WV3k6x0MrlcSjzRwAfFiUfmg5gilZLmmu1NquJtxvTWtFSwwH9OsOmhs19a8GyMBOTC35U1VQgySnSdxEMG7926QJbAYAV4q5s8iawdHRWxNAd4AgKfc8DCEmeGqqGE2VKkp6bTe/pxaEVtTU8dkpWEo91rKLx22Ulz15iJ09dTDS795cfRaYdGN5XCh65DzIoqta5vhR8px2aV/VdhTGj0fKPyJyEKktAlU3xN8zWOWDsiHkenoYmbvSXBwyROhVjfJB9WedaMYyg21UeZRAZt4+L+ZyE3binjf7vFC72hE9xxu2j1xUhndG8XI+/aG2GtU+cVedGuEJV46unHf2kYxRkcxrgnhMZhxLHxF8YMFvGpkdVJk87JY64dFyRotYwuKnOlZP1CH57Z5jVG26QK5BiqbC83pzJ34MCx75Qf49a6nSkDPKV2Uit+s0bvHPpnKRK1gQHYyOdNPbNwuqsdUox041RMdjHQBkdmS6SCxuqFYZDi7uvr/3easaeM2t1pNp19gub5CW5TRP8C4FwztW+3V/2GimzkJFx96yAqo6f8Vi2EZ+VcZ0I2+0bs2PQ9xR04dmJBJZcY9AycyDduviXARwwElRsmLhjNSlAmJgmRnZWd56nC5xoF0fYWBj6hP0SNqcWIFS86pHRkMS1sFcLjhoiTmzvKqjIw9q1D53GwLlBOgGcPIfCkHKSd51X9+j+N8wj6zb8u4+43MvN8EXv8SYRYWkAgKM7slcbZL7f9IZBYnv/g6S+PZo04Nu+2g08/7cvaRCC2rUxxRlZC1HCaESbOhb/ULXPOuEz7mySt/Kq3tfaC/JlbZcll61nOIfTudmQ1BwAWnOA77oFc7qsCIpVBmlscRzDb3cnylkFzrReoY/x58VqQakMnOdDtv711OIPTIsHepdCJ4MMkJgdi7VMXrDfDMxgVfw9XKq1H2jYWdCZiY/0f+fXHfW6GqvHxeE6CdZAK6fE7FnyOxKWRUC1VXgELdu5vvgDfySjPjglcpJEYFCMV3VS1Ft8JDT2TTqtd1ZtRmn/4zBUeG1Ca2vrHdn1JJS9jNR4X4H1uqYo2pgmNZuVtf7b2WIR5z2g+WJiNU0D+/Bj3IX5zO5KxonKt2rTpBlqFyuAzkHrnyPWgjWPdP5gND7+pCJPbQqlM8yAOA62kpquudDWcBUYPRCHjJzWnzzIM3ZH/7lOQZKkn8f5hrc3i3VWtNTnpfI2S4YJ6DaOC+B2/MmYjYbLMg5wIKy+D2VkZerVRaU4njheXODwU34AyMycOKMjE9f9RRx+YDTIHqFxE3/kr1pho3rHu2R80EqdYBmmNz1pw067wmslSbdhR6bY55YoBypSLHM3YJL4t3EHpTzq4SjcfC1cUDSTa+bamIoISfAKKiwGYuMHGDeJga4ZEWZfKPH8OfvpslfeoCftkm+IKL6VR8BZmU940TVTtB54vwQK5LRY5D3e2uzHbu6On0IUfmzgme/Wvy6nYR+Zux4Tev8JPVjAC2ivEMjfhiCPtL/UgYsMvDW5veKs3C0AqbMggtjqt+2hunKCnBKODtF/y5IGPoK1KZ0/w5SpGCQuUrP2+dFqwivx99gkyL7saVEFTJ7mJKMKDJQEeoZ6+lfqVXZAjGhTWc2ovlPoKgyCUOxfqkPypFuU70ZxssK6UOacGC9pNTIGvESPawuAzoGKeeVFFbH6EJS2l0gB8w9ix6QkT8o5Him8w2a2uDI2YOCgUdjUsNAlqnte5lCnJvrwEuRkG74dUsTARxUBL/HJirZD7O0LualOr9nI/LJ4zjqxYZ9iVjyvo++vGyls1EN5v5qhqq+D3AMGEVtQ19Cqzo+okc6rg2ZOhm1IekUEjYXSTczM0nzEA5a14ta8qMh1UFRWwD0YyP1oy1qQRS9zHsPJAyFWkwkg9hQh8HI2nx6VUP8gifOelhvzz11y6cDLo234p7hIJcIgC1rKtNDHlcn5NfxwYCIgQdRB5b5+JzhIDoQLlCKhiJpF0TO6xuDkFqk1Il+9eq51YItPunOyvsuGZr6b8wNx+YWCwwEwNOPV+t1vkYfNg+IfOo97hwMjkRmDU3Ixh77fGEL4zc/31yptvSZl+wIOVjYz20ihjV4IQdZ+xcIIh4+dptV5kFyqXHqFdXV8NG2BIh6Mw+bv5ccUW3+3Z3uqt4iaGdUCi6BNGa6IV4wzcAVbjv7caG9f8TlcFPPhoQLLMUgQUdMXV+3Nk8ytRiuPKxZyIWcRSw6hpA5KoHVJPh9PiqAkpg0Q4RM5qjJjS4SYdYza6UrCT5DnljmKZvdf/8ior7z0UtFBTgllRnGd7isKyeCoHXDCVDxShp71ImG5Br6VsqwHI5oDo+pZZPoKlOGFpHdV0GQ2KUM6Fwq6jD78damIY4J14/zVVJWA968oQFRJOe2c5lHgHZC+YIF3TP4bekR6eVXD+DXEhC9P1xfjKA+fhrEBkxyfkAd05eLhWRv98l/Z21Ozb4y9bJyxo+2KWX9Td6gCNLP4tuk8XOePO8oNGn0JFx4HuI3f8vEQ1ChjW3+ZOzvuDNYFqsF4Wbj9ol+IkGMWAColXfROFEgRDTLNzJyEv3JgtpKRuYSMi0Sz/hMSwUgJy/FDRlT4k/orJWx2KjuADH4iY8lmADeWHtWH6AMmDWwir0L5ykMlOon0TMhgm9sseBtkwggZ1QbZfpGNWwq56y9FLUyNxYH2kXOBMIeh47hgg3XVKyAMymVRH6Aht0lPrVJqjnFsWFg5lX8V3WMNwSiyK0JcEfxFvCmiDdmsB2xg9NQpNcyB52P7x2MWLfLF2zAXI2AvPozmRSjED0JV51FEDKJtq+o7reN8b9Xpt+X7TnBCPFQCS8cfgmkYnZW/WUAvOX2DPFbh0HNBiGGzALpyR/0pqzPon54aQX0RvMTrC5ZHlUew6+9Bt36Yt4ROXzH4cVMui5FzkeLLdpNmQ+xzjaJSTtX8BYgNeuRd8K9Us2wniUCbxYiq2FUMI7/AOeIrTNywavknnPnWl3HYSOdJspLyjSmJU22jIc77SiTagsxRBekwsMrdI3RmXj1vdm7RGOLglky4jRAGboUQ9Nj1hkbNoTUzUd7J7/0QkTuwgJ3pSwEx5unjK4vBo9XLqpyBGYFGpH2xvaWrPfnTCLHkj0ehyrP6zOhQMikPMrfbOui3rAilYf5q8M/kGtvFH1MADZRMplBMJ2PMmXnqHkqlPOq7Z/sf9OcdBF82wqT+/srfNgGPG5pUGMtwQQKHtC/JMN4NnKZlOrYbf3tRRQRglAeIisDduciupLm+HDkmnP6LNvvZRO8jl3ej09jkq4QNShuhwQzpqoBcbgTXsNmb5QwT03xg0zTWQIOpF7ycKySRvtsHEk+uP4r0nfbN68eH7HQ3liyVKTVcQnMIUtS6iw5pDFpHwg+hWOAVQw3QFC7YDpt0yW5fiXwcbmTyl817HfaQsOAloQWxRqrnkTMrr5fDyZQzpYMJPfyHCQD3pML/zzmo9qXDm2M9atn1JpvIo9TwwDd2c8YJxY4E+ErtHO5bo+MdKpGEeuGhiDpecIQui5HkcOTX7z+Pef05C/VIEwdWOKg8EU0vEic4ocE93sysQUFKf7P37tWOZSYG94jW9mx8CFcEyqthFKr47JkSi04hKzLIgiXHLq4js4yBNG3f8TR7vdqpAsMoKLYDL9deUECC4lz7uaGhLUGATQjc6+GyeSZz02C6RRNGcV6rv+oVW0wureC8fUUX2MWHKFw1A2WVZRGsmOBtKTHpw5nTjcbJDafrOWIE+nkWMTY/GYhat0grF4zepa8DW+s1m0QaBRWBRifO3+mz09iTuMF5z7niBGVFYc/Vak6h5hTbZgpDivaqdGSBIUWjituQ61R7pgpy3/9uOVpXQROtk/1ryszL0vUMzYWNZrKJyhqYGGTtHHuXsEx9GWz3R/kaTOgXgsQWDpWwKAN4azbtBlfgUu5cTfVNhW0zc/TIypcrab01Pjz58elmucu+bWalOO6LHpU5QFf46Tx20TMFIUWAscBPWeKJpTFA3brW1OX3xaN/ULKy3aeQ2A/jih4Nkher6T8PTVR5SQ92k5hE6JFhBLSeqzaXzNBBVLz7gBEk4Imefb9a6VeKSZ1pZEENiiZOIYBwAn/ViHeT7+KX7kRaruFXumYuWcqP23cXX35t8Yic7rY2PgWbhE+8Ytkfa881mPGZMmHua48QVRR1suV+L0upZDhAbT6oX0tjLrJESBKdZcfqKou2qO38z8ilDAe+3ijrrdxOZN+qe6cBWzIKLFxDNNsdo8KPBOQMIT3ZsLHA2DNgGjdO3yPv3W+Nt/puJlNGePo+WcUcSrqqXmh5LrBLWRFpQoUwHhTPrW8QMzg48AfSr5Mdm3iWeQaobRAUc5yolsHu6rc9jTFszpncCdmln9k9lBLGpHjmjbcuiyGCIlVr7kXk4LikxiT/nG3kEnD90LfqiLgozb1EOD5vMDrz6bQl9IA5N02J51w0Aje66IczXA6/ceqB1jAWAGmB1b6UMisxfgpcuJpcxAni8mVUUAaVOxeovA8Uh3BZACMkjLYYaxn7IOG3J4lxbH8qJsb+JmAEkvsjOeXp+G9g+EnQ/YFP4Pzt3xFGJg6O6bnw//DPnqCgZhL/39RBxJDa3sKIh3oYFj/8pHAfqxojWKZomk6POYcZB538FbcD1njX7+pNPYwIsFKF+nJwiHLm+b1UrNyVPBA8lyR4MylVHgNxeI5bqhR4fZAEzqD7h1yI7PWPUNp5N0zRChJozyWN0jGh72Zz7ppP0HGcfFYOR1HL3MQ2ttyNf+XPPX/UqQl9hkcmAzPYZD4mTpA4dMs+BzwCclJwt6Gfa/nn8rhqrSQteNkKlWZ8gkgSPpwWVVGEDhKFZTG348pFIDX8G69+nMzfqIzC+JYKpq5uBXTwdlW2f3nSsOIaqrdGSOYiD96V7Fg52cH/HK7YQWvpJYccU5gln2aIETgqMDko/6UCWnoHRgJWeHoojdKr6gUOoDbXmXxS8l4V+lG0Fy9KIvusxKa47wCcvpSyAS9mqkuPESOOiLcsoYUasXbqw5lgSQ2iswd4ntV3xh9LWBJbgzWtjhJ/M89mQ32tUGBv+2m8XUu62ydp6aevnFJDA/ZA8NPj9ruTOOy/MDmo5SNmsAFtX1CYWWmVXJmwv4PsS4EhzB5E3oVHvHUgDlfSjY3x+xDsemH99bxXJQd5133kuCsMb9lxPMCYEoN1/qshSWpXozNXBOLmhPrREIZpFCxvOFaakTC+H9NxEgdvOWWb7oHY3+mqshIPHeV0fISySIFi7khkMwl0w4ktkolinx8PjhOid6U1cT11gckd6W25g2a7XX6xv6uS/PYbSvH083Wrru6Fkxr8ehwK9hmY7ei+OZqeljcFWNSbMuf1FbvQgztqLj5eRJb4bVwTqYGvFYSgP5KBekHe7e40T1qrJO87qRiMvxqZ5BWCSq/oqMo+3+7vafGSi8t4jE1SEvlI48ENzgVyDs7oeOh7IbagUflPfEHb55bs8t+HTDkcFefDFbOI83yQ/VU7Fsb2d2qjaOh7hV8Itj/t0jPQh1ksjYqJSfa91kzvlj3mpE68Q1dilnN4/saA4ZkrMdUvv1+yLMeO6pYXR2Lgs/Z1VY96N0dvpgpJ+f/wMsIz0D4IdEh+mZn/kq8Z5Pv1nuasBPfKWf3OP/a0c9rjNgHSFwPVmw/l7tDl8Hl9IKOUO2yYmIEGiA2nspXfaU/cfmfbhPltvq3C6sLG2OovBj4L4fzlA3enT+ytqXF1oSpXwkHhtB1VPvnMYpT8x6mEwBq3Ao0NmdEDVOQ15xUJ+50DH5LkiG5bm1YxyqGoXSs9RjctnZgfoR8sXR64jCK2t1RksSZDdbCAokp7NMuhSk/jMI3N1NmOvWrsFKJxZajjNjUKLmBpz/WP1tYpU2xslBXZTr88lsupeR/PE72YAnju63aoBGPr25aonpGuschEjyc6aNaDImodB9xHAXI09HKqR0FVrVU59Xey7rH1/gL4aGRjQ+8bYuwv0RPh3H6cEo/E0CSDI4g2KmpyRbXzCz7TNQjPhNvgLyku8ajVAmKIW6zVcCkofNZSySqc/ndrFcmpg3HE5XUP0BzigcQ47sy8VoMO1wgVuapv5lRYo2dixH39hOWAmtn6Fr3eZ5lFqKuQasMdlujPt2Nq4Vf2dkyFmdPAOnojeHO9dKew9yi5nF1N0zeh3U38kEVLrr579jcMjAnFkokqAg5u8PNOzHHZg1V7PtYW1ZZbx+SFZ1MmgO/d3UVNEe/i9KB65DIB6dJPHy8KJ1N702PDbCtaFTnKPfm65+3lo5xEBGE7QNxxQ3U/4/9tPEj/VKyxi+T9E5uz8DxY+EbGv3W6X67+MYD/92xgOCzBj114glTLUD0uhJCJV+o+sOxW7Wk3bt7E8TqR1MOQqERu7goqUc/HZBw7yckI15//fBLMmnSbig+tO4EU6p3PnhjKoxmCz8KSsS5Z/leJwBZcJct1XPfWryfnR/FFuOXdVPTjFp6GI50BBQiUtQ+uv8NOx4yttcMvg2DT1WwYpepCEj9Ph2AlAsvm61BicW+21de+2FJHKFaF/U89nJr6m9XZ/SJ6HlYlKqWgVELGyKfh7Sc045ESoXlewPpUNQnPJbV+6iwRH+eupoK2+A5G53gTHcD1QhgxSXF8KnyuaSLbxuE9VAfCQveHkmMyflU4a0zV9FvpDmH0BJBvcQmVmtfeCCSbGFf69x921MAkrj8Ob+1g4KrkYeRM4ucyQJGAp2TnFCdy5oTyrDBj2M9qMz2z7DwvCg6VKd0nUqLz4AanRXUB1YvQGN8joKgPqwbTma/d2wpPgsNUSECKRa0J+CLyKpFcpzAVU7Db/DxOxmsN48OzcZhibT96xLTCnDWzZ5fUEROluPhuXPwU2CYNl2aNYpeBWVnu5C7NNGHcCN70w1zChC3MeTKxN0Zm0BHBAPCjISy0lb3DBJyO4zuPDnTY/sbVQLFlavqPHax02NhXTd5FJRuVMrGsIZsmk6uYlyV+rs8z/3oWm65sv+JzM+ij6cAmEv9vqRxK6x0An2pwNcwI+uf2KZ6xvFyeMku+Ww8nv7XMU1l7dJ6kRilNL3BMn2UNdCC8pXYaC9bIOzhKm6EA+sM0k3Q6m97FlLOrk12rAGLnCIujHJtpyLrp4Cl/xbwZl4tv93uaHy6+owiHab0xXlV6GhxJMxV03LTS9QLC57rVkf/TkUHFY/pL3t8GiaaTXtTA8OyKDJLAViz1iWzmiXBi6GZsVcWvyjAn917na9VBhtgxTAzf1uTBPoK8hjcZ9tDacia5BYVjsdkgPVaVN973N+6/OCi+vy+EMXXkCw5ndrJktBkObnFxCzNjzK1Rf5wIL4++OtaxaYEwHV5QNRTZidHJts2650mTADP85nlGOxQDRcDOnpGqEpAReOdlba2dfWlz7CrWGl5KSW//aIfhu17RfTI5jorZrm1LtaEhn3GdvlbVKc3Z9/OQe94+cXhhuaOxW4Yx9NX/HxwNDHwHetVpie2TnZESYiy+LT9gXOdtIGA35XtRsp1I/hZLTZVLLPc0+8TakqzoBLalokx5mds3bytYCejA07p7xraTWZzudINdrWrU8Vz0or74IranmksecYBme0GaVSxnt2B2XnCdyC/9tHw3o5pI4qvBSQQQ8QNALYCxyprXpHTAS4vdaD+22fLco7j3rnxMFI7Tqv/qjvACWe3J/TmbhatdsKKVURCC9HyaHZypEwR2xI/jA1eN4jBeh+YGKYG5ZEIpSd/XlgWJyq83sVNYb7cDdtNGD/VvvlEEh2M6UuXbc70daXYYhq4kNtZ4N20/nGyuN5tUYzmRkVzngoonHsOwf3Eurlmhiy3UCkcXVEfxGCQCFnka/zm+a7XRqpFOA8yUHuIXF7DKCfFBVe4IgQY+l//yWgIOa1o/yRpflNWJlN3E4UWlKYbn0bYkr5wjfm1sJ35bpsSoaLqlL2hOQss6aGHGPOLdvXyEttffS6T9VUgH5BMNYFoIBmB0dv7i8nx4PrT0CNSuif4hvNpGh2eGAuD0mgij19Q2z9GQVKwsPPeLsKip8FCZLn0o61MIMuxCHgL+OzGU/0+2P0BPCb9M4GqOSfSQoiK7FVDtIYPImUL7gyqDQD+YKJ3jkLhrxCKHDbJqtNY1GmAlOtmQKmKJrbAJy7KddPu6DwuO5eD79IiJAdVDyTnn6DGte1rf6kDwD4yNSvTQTL2EhzhxI4CAnzEd4eSrhI5nvaixp+3cut9T+GD1RDzocBEMbPxS2rGXqVupTdJyThG5R+bGCGzJV8iwPTB0AHS/8u+yRvFvonUUDc3AdoMjkIoMlsHcS6z2wY3qgInIkyo1y9hsOwldajV8SHmOg7jyo2ZwAaKny/dgD7APLyHHuP9MgKwlJBFi4RG+8A9ghUHPxgoiWQ4xDekBEDK7ClGsxHmISFx3TGaa4mdT5YuYOaWwjaQw0hjU3EVfyW6p5CRi1OLOrew/MYAQ=="}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":565,"cache_read_input_tokens":75180,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":565},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYenigjHdVX8mfX9o9RB5","type":"assistant","uuid":"83e31294-060c-44e8-89fa-051e748aa0b6","timestamp":"2026-03-02T19:16:24.020Z"} +{"parentUuid":"83e31294-060c-44e8-89fa-051e748aa0b6","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01KmocL94wUbWJ4RMi4dcu1d","type":"message","role":"assistant","content":[{"type":"text","text":"Right, the user could provide something like `/data/store.db?cache=shared&mode=rwc`. We need to split on `?` before doing path operations like `filepath.IsAbs` or `filepath.Join`, then re-attach the query params after."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":565,"cache_read_input_tokens":75180,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":565},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYenigjHdVX8mfX9o9RB5","type":"assistant","uuid":"cfd516df-51b6-43c8-bbd9-aa03acd864f9","timestamp":"2026-03-02T19:16:24.377Z"} +{"parentUuid":"cfd516df-51b6-43c8-bbd9-aa03acd864f9","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01KmocL94wUbWJ4RMi4dcu1d","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Ar5kuagxhuwJ1PGEouz46J","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go","old_string":"// NewSqliteStore creates a new SQLite store.\nfunc NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tstoreStr := fmt.Sprintf(\"%s?cache=shared\", storeSqliteFileName)\n\tif runtime.GOOS == \"windows\" {\n\t\t// Vo avoid `The process cannot access the file because it is being used by another process` on Windows\n\t\tstoreStr = storeSqliteFileName\n\t}\n\n\tfile := filepath.Join(dataDir, storeStr)\n\tdb, err := gorm.Open(sqlite.Open(file), getGormConfig())","new_string":"// NewSqliteStore creates a new SQLite store.\nfunc NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tstoreFile := storeSqliteFileName\n\tif envFile, ok := os.LookupEnv(\"NB_STORE_ENGINE_SQLITE_FILE\"); ok && envFile != \"\" {\n\t\tstoreFile = envFile\n\t}\n\n\t// Separate file path from any SQLite URI query parameters (e.g., \"store.db?mode=rwc\")\n\tfilePath, query, hasQuery := strings.Cut(storeFile, \"?\")\n\n\tconnStr := filePath\n\tif !filepath.IsAbs(filePath) {\n\t\tconnStr = filepath.Join(dataDir, filePath)\n\t}\n\n\t// Append query parameters: user-provided take precedence, otherwise default to cache=shared on non-Windows\n\tif hasQuery {\n\t\tconnStr += \"?\" + query\n\t} else if runtime.GOOS != \"windows\" {\n\t\t// To avoid `The process cannot access the file because it is being used by another process` on Windows\n\t\tconnStr += \"?cache=shared\"\n\t}\n\n\tdb, err := gorm.Open(sqlite.Open(connStr), getGormConfig())"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":565,"cache_read_input_tokens":75180,"output_tokens":3034,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":565,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYenigjHdVX8mfX9o9RB5","type":"assistant","uuid":"962b799a-edbd-4945-a9ff-13ee3d250238","timestamp":"2026-03-02T19:16:25.875Z"} +{"type":"file-history-snapshot","messageId":"962b799a-edbd-4945-a9ff-13ee3d250238","snapshot":{"messageId":"d4eea3a0-189a-4500-b784-8e496ce35a38","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v2","version":2,"backupTime":"2026-03-02T19:15:31.656Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v1","version":1,"backupTime":"2026-03-02T19:17:53.586Z"}},"timestamp":"2026-03-02T19:15:31.655Z"},"isSnapshotUpdate":true} +{"parentUuid":"962b799a-edbd-4945-a9ff-13ee3d250238","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_01Ar5kuagxhuwJ1PGEouz46J","toolUseID":"toolu_01Ar5kuagxhuwJ1PGEouz46J","timestamp":"2026-03-02T19:17:53.603Z","uuid":"ed981c98-2a0d-426f-be10-1332ece7d39e"} +{"parentUuid":"962b799a-edbd-4945-a9ff-13ee3d250238","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Ar5kuagxhuwJ1PGEouz46J","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go has been updated successfully."}]},"uuid":"5b922543-3105-449a-b4d5-979335b5aefe","timestamp":"2026-03-02T19:17:53.601Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go","oldString":"package store\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n\t\"github.com/rs/xid\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gorm.io/driver/mysql\"\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/driver/sqlite\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n\t\"gorm.io/gorm/logger\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/management/internals/modules/reverseproxy\"\n\t\"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs\"\n\t\"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain\"\n\t\"github.com/netbirdio/netbird/management/internals/modules/zones\"\n\t\"github.com/netbirdio/netbird/management/internals/modules/zones/records\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/telemetry\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/management/server/util\"\n\t\"github.com/netbirdio/netbird/route\"\n\t\"github.com/netbirdio/netbird/shared/management/status\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n)\n\nconst (\n\tstoreSqliteFileName = \"store.db\"\n\tidQueryCondition = \"id = ?\"\n\tkeyQueryCondition = \"key = ?\"\n\tmysqlKeyQueryCondition = \"`key` = ?\"\n\taccountAndIDQueryCondition = \"account_id = ? and id = ?\"\n\taccountAndPeerIDQueryCondition = \"account_id = ? and peer_id = ?\"\n\taccountAndIDsQueryCondition = \"account_id = ? AND id IN ?\"\n\taccountIDCondition = \"account_id = ?\"\n\tpeerNotFoundFMT = \"peer %s not found\"\n\n\tpgMaxConnections = 30\n\tpgMinConnections = 1\n\tpgMaxConnLifetime = 60 * time.Minute\n\tpgHealthCheckPeriod = 1 * time.Minute\n)\n\n// SqlStore represents an account storage backed by a Sql DB persisted to disk\ntype SqlStore struct {\n\tdb *gorm.DB\n\tglobalAccountLock sync.Mutex\n\tmetrics telemetry.AppMetrics\n\tinstallationPK int\n\tstoreEngine types.Engine\n\tpool *pgxpool.Pool\n\tfieldEncrypt *crypt.FieldEncrypt\n\ttransactionTimeout time.Duration\n}\n\ntype installation struct {\n\tID uint `gorm:\"primaryKey\"`\n\tInstallationIDValue string\n}\n\ntype migrationFunc func(*gorm.DB) error\n\n// NewSqlStore creates a new SqlStore instance.\nfunc NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tsql, err := db.DB()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconns, err := strconv.Atoi(os.Getenv(\"NB_SQL_MAX_OPEN_CONNS\"))\n\tif err != nil {\n\t\tconns = runtime.NumCPU()\n\t}\n\n\ttransactionTimeout := 5 * time.Minute\n\tif v := os.Getenv(\"NB_STORE_TRANSACTION_TIMEOUT\"); v != \"\" {\n\t\tif parsed, err := time.ParseDuration(v); err == nil {\n\t\t\ttransactionTimeout = parsed\n\t\t}\n\t}\n\tlog.WithContext(ctx).Infof(\"Setting transaction timeout to %v\", transactionTimeout)\n\n\tif storeEngine == types.SqliteStoreEngine {\n\t\tif err == nil {\n\t\t\tlog.WithContext(ctx).Warnf(\"setting NB_SQL_MAX_OPEN_CONNS is not supported for sqlite, using default value 1\")\n\t\t}\n\t\tconns = 1\n\t}\n\n\tsql.SetMaxOpenConns(conns)\n\tsql.SetMaxIdleConns(conns)\n\tsql.SetConnMaxLifetime(time.Hour)\n\tsql.SetConnMaxIdleTime(3 * time.Minute)\n\n\tlog.WithContext(ctx).Infof(\"Set max open db connections to %d, max idle to %d, max lifetime to %v, max idle time to %v\",\n\t\tconns, conns, time.Hour, 3*time.Minute)\n\n\tif skipMigration {\n\t\tlog.WithContext(ctx).Infof(\"skipping migration\")\n\t\treturn &SqlStore{db: db, storeEngine: storeEngine, metrics: metrics, installationPK: 1, transactionTimeout: transactionTimeout}, nil\n\t}\n\n\tif err := migratePreAuto(ctx, db); err != nil {\n\t\treturn nil, fmt.Errorf(\"migratePreAuto: %w\", err)\n\t}\n\terr = db.AutoMigrate(\n\t\t&types.SetupKey{}, &nbpeer.Peer{}, &types.User{}, &types.PersonalAccessToken{}, &types.ProxyAccessToken{},\n\t\t&types.Group{}, &types.GroupPeer{},\n\t\t&types.Account{}, &types.Policy{}, &types.PolicyRule{}, &route.Route{}, &nbdns.NameServerGroup{},\n\t\t&installation{}, &types.ExtraSettings{}, &posture.Checks{}, &nbpeer.NetworkAddress{},\n\t\t&networkTypes.Network{}, &routerTypes.NetworkRouter{}, &resourceTypes.NetworkResource{}, &types.AccountOnboarding{},\n\t\t&types.Job{}, &zones.Zone{}, &records.Record{}, &types.UserInviteRecord{}, &reverseproxy.Service{}, &reverseproxy.Target{}, &domain.Domain{},\n\t\t&accesslogs.AccessLogEntry{},\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"auto migratePreAuto: %w\", err)\n\t}\n\tif err := migratePostAuto(ctx, db); err != nil {\n\t\treturn nil, fmt.Errorf(\"migratePostAuto: %w\", err)\n\t}\n\n\treturn &SqlStore{db: db, storeEngine: storeEngine, metrics: metrics, installationPK: 1, transactionTimeout: transactionTimeout}, nil\n}\n\nfunc GetKeyQueryCondition(s *SqlStore) string {\n\tif s.storeEngine == types.MysqlStoreEngine {\n\t\treturn mysqlKeyQueryCondition\n\t}\n\treturn keyQueryCondition\n}\n\n// SaveJob persists a job in DB\nfunc (s *SqlStore) CreatePeerJob(ctx context.Context, job *types.Job) error {\n\tresult := s.db.Create(job)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create job in store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create job in store\")\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) CompletePeerJob(ctx context.Context, job *types.Job) error {\n\tresult := s.db.\n\t\tModel(&types.Job{}).\n\t\tWhere(idQueryCondition, job.ID).\n\t\tUpdates(job)\n\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update job in store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to update job in store\")\n\t}\n\treturn nil\n}\n\n// job was pending for too long and has been cancelled\nfunc (s *SqlStore) MarkPendingJobsAsFailed(ctx context.Context, accountID, peerID, jobID, reason string) error {\n\tnow := time.Now().UTC()\n\tresult := s.db.\n\t\tModel(&types.Job{}).\n\t\tWhere(accountAndPeerIDQueryCondition+\" AND id = ?\"+\" AND status = ?\", accountID, peerID, jobID, types.JobStatusPending).\n\t\tUpdates(types.Job{\n\t\t\tStatus: types.JobStatusFailed,\n\t\t\tFailedReason: reason,\n\t\t\tCompletedAt: &now,\n\t\t})\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to mark pending jobs as Failed job in store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to mark pending job as Failed in store\")\n\t}\n\treturn nil\n}\n\n// job was pending for too long and has been cancelled\nfunc (s *SqlStore) MarkAllPendingJobsAsFailed(ctx context.Context, accountID, peerID, reason string) error {\n\tnow := time.Now().UTC()\n\tresult := s.db.\n\t\tModel(&types.Job{}).\n\t\tWhere(accountAndPeerIDQueryCondition+\" AND status = ?\", accountID, peerID, types.JobStatusPending).\n\t\tUpdates(types.Job{\n\t\t\tStatus: types.JobStatusFailed,\n\t\t\tFailedReason: reason,\n\t\t\tCompletedAt: &now,\n\t\t})\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to mark pending jobs as Failed job in store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to mark pending job as Failed in store\")\n\t}\n\treturn nil\n}\n\n// GetJobByID fetches job by ID\nfunc (s *SqlStore) GetPeerJobByID(ctx context.Context, accountID, jobID string) (*types.Job, error) {\n\tvar job types.Job\n\terr := s.db.\n\t\tWhere(accountAndIDQueryCondition, accountID, jobID).\n\t\tFirst(&job).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\treturn nil, status.Errorf(status.NotFound, \"job %s not found\", jobID)\n\t}\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to fetch job from store: %s\", err)\n\t\treturn nil, err\n\t}\n\treturn &job, nil\n}\n\n// get all jobs\nfunc (s *SqlStore) GetPeerJobs(ctx context.Context, accountID, peerID string) ([]*types.Job, error) {\n\tvar jobs []*types.Job\n\terr := s.db.\n\t\tWhere(accountAndPeerIDQueryCondition, accountID, peerID).\n\t\tOrder(\"created_at DESC\").\n\t\tFind(&jobs).Error\n\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to fetch jobs from store: %s\", err)\n\t\treturn nil, err\n\t}\n\n\treturn jobs, nil\n}\n\n// AcquireGlobalLock acquires global lock across all the accounts and returns a function that releases the lock\nfunc (s *SqlStore) AcquireGlobalLock(ctx context.Context) (unlock func()) {\n\tlog.WithContext(ctx).Tracef(\"acquiring global lock\")\n\tstart := time.Now()\n\ts.globalAccountLock.Lock()\n\n\tunlock = func() {\n\t\ts.globalAccountLock.Unlock()\n\t\tlog.WithContext(ctx).Tracef(\"released global lock in %v\", time.Since(start))\n\t}\n\n\ttook := time.Since(start)\n\tlog.WithContext(ctx).Tracef(\"took %v to acquire global lock\", took)\n\tif s.metrics != nil {\n\t\ts.metrics.StoreMetrics().CountGlobalLockAcquisitionDuration(took)\n\t}\n\n\treturn unlock\n}\n\n// Deprecated: Full account operations are no longer supported\nfunc (s *SqlStore) SaveAccount(ctx context.Context, account *types.Account) error {\n\tstart := time.Now()\n\tdefer func() {\n\t\telapsed := time.Since(start)\n\t\tif elapsed > 1*time.Second {\n\t\t\tlog.WithContext(ctx).Tracef(\"SaveAccount for account %s exceeded 1s, took: %v\", account.Id, elapsed)\n\t\t}\n\t}()\n\n\t// todo: remove this check after the issue is resolved\n\ts.checkAccountDomainBeforeSave(ctx, account.Id, account.Domain)\n\n\tgenerateAccountSQLTypes(account)\n\n\t// Encrypt sensitive user data before saving\n\tfor i := range account.UsersG {\n\t\tif err := account.UsersG[i].EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn fmt.Errorf(\"encrypt user: %w\", err)\n\t\t}\n\t}\n\n\tfor _, group := range account.GroupsG {\n\t\tgroup.StoreGroupPeers()\n\t}\n\n\terr := s.transaction(func(tx *gorm.DB) error {\n\t\tresult := tx.Select(clause.Associations).Delete(account.Policies, \"account_id = ?\", account.Id)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\tresult = tx.Select(clause.Associations).Delete(account.UsersG, \"account_id = ?\", account.Id)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\tresult = tx.Select(clause.Associations).Delete(account)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\tresult = tx.\n\t\t\tSession(&gorm.Session{FullSaveAssociations: true}).\n\t\t\tClauses(clause.OnConflict{UpdateAll: true}).\n\t\t\tCreate(account)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\t\treturn nil\n\t})\n\n\ttook := time.Since(start)\n\tif s.metrics != nil {\n\t\ts.metrics.StoreMetrics().CountPersistenceDuration(took)\n\t}\n\tlog.WithContext(ctx).Debugf(\"took %d ms to persist an account to the store\", took.Milliseconds())\n\n\treturn err\n}\n\n// generateAccountSQLTypes generates the GORM compatible types for the account\nfunc generateAccountSQLTypes(account *types.Account) {\n\tfor _, key := range account.SetupKeys {\n\t\taccount.SetupKeysG = append(account.SetupKeysG, *key)\n\t}\n\n\tif len(account.SetupKeys) != len(account.SetupKeysG) {\n\t\tlog.Warnf(\"SetupKeysG length mismatch for account %s\", account.Id)\n\t}\n\n\tfor id, peer := range account.Peers {\n\t\tpeer.ID = id\n\t\taccount.PeersG = append(account.PeersG, *peer)\n\t}\n\n\tfor id, user := range account.Users {\n\t\tuser.Id = id\n\t\tfor id, pat := range user.PATs {\n\t\t\tpat.ID = id\n\t\t\tuser.PATsG = append(user.PATsG, *pat)\n\t\t}\n\t\taccount.UsersG = append(account.UsersG, *user)\n\t}\n\n\tfor id, group := range account.Groups {\n\t\tgroup.ID = id\n\t\tgroup.AccountID = account.Id\n\t\taccount.GroupsG = append(account.GroupsG, group)\n\t}\n\n\tfor id, route := range account.Routes {\n\t\troute.ID = id\n\t\taccount.RoutesG = append(account.RoutesG, *route)\n\t}\n\n\tfor id, ns := range account.NameServerGroups {\n\t\tns.ID = id\n\t\taccount.NameServerGroupsG = append(account.NameServerGroupsG, *ns)\n\t}\n}\n\n// checkAccountDomainBeforeSave temporary method to troubleshoot an issue with domains getting blank\nfunc (s *SqlStore) checkAccountDomainBeforeSave(ctx context.Context, accountID, newDomain string) {\n\tvar acc types.Account\n\tvar domain string\n\tresult := s.db.Model(&acc).Select(\"domain\").Where(idQueryCondition, accountID).Take(&domain)\n\tif result.Error != nil {\n\t\tif !errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\tlog.WithContext(ctx).Errorf(\"error when getting account %s from the store to check domain: %s\", accountID, result.Error)\n\t\t}\n\t\treturn\n\t}\n\tif domain != \"\" && newDomain == \"\" {\n\t\tlog.WithContext(ctx).Warnf(\"saving an account with empty domain when there was a domain set. Previous domain %s, Account ID: %s, Trace: %s\", domain, accountID, debug.Stack())\n\t}\n}\n\nfunc (s *SqlStore) DeleteAccount(ctx context.Context, account *types.Account) error {\n\tstart := time.Now()\n\n\terr := s.transaction(func(tx *gorm.DB) error {\n\t\tresult := tx.Select(clause.Associations).Delete(account.Policies, \"account_id = ?\", account.Id)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\tresult = tx.Select(clause.Associations).Delete(account.UsersG, \"account_id = ?\", account.Id)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\tresult = tx.Select(clause.Associations).Delete(account)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\treturn nil\n\t})\n\n\ttook := time.Since(start)\n\tif s.metrics != nil {\n\t\ts.metrics.StoreMetrics().CountPersistenceDuration(took)\n\t}\n\tlog.WithContext(ctx).Tracef(\"took %d ms to delete an account to the store\", took.Milliseconds())\n\n\treturn err\n}\n\nfunc (s *SqlStore) SaveInstallationID(_ context.Context, ID string) error {\n\tinstallation := installation{InstallationIDValue: ID}\n\tinstallation.ID = uint(s.installationPK)\n\n\treturn s.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&installation).Error\n}\n\nfunc (s *SqlStore) GetInstallationID() string {\n\tvar installation installation\n\n\tif result := s.db.Take(&installation, idQueryCondition, s.installationPK); result.Error != nil {\n\t\treturn \"\"\n\t}\n\n\treturn installation.InstallationIDValue\n}\n\nfunc (s *SqlStore) SavePeer(ctx context.Context, accountID string, peer *nbpeer.Peer) error {\n\t// To maintain data integrity, we create a copy of the peer's to prevent unintended updates to other fields.\n\tpeerCopy := peer.Copy()\n\tpeerCopy.AccountID = accountID\n\n\terr := s.transaction(func(tx *gorm.DB) error {\n\t\t// check if peer exists before saving\n\t\tvar peerID string\n\t\tresult := tx.Model(&nbpeer.Peer{}).Select(\"id\").Take(&peerID, accountAndIDQueryCondition, accountID, peer.ID)\n\t\tif result.Error != nil {\n\t\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\t\treturn status.Errorf(status.NotFound, peerNotFoundFMT, peer.ID)\n\t\t\t}\n\t\t\treturn result.Error\n\t\t}\n\n\t\tif peerID == \"\" {\n\t\t\treturn status.Errorf(status.NotFound, peerNotFoundFMT, peer.ID)\n\t\t}\n\n\t\tresult = tx.Model(&nbpeer.Peer{}).Where(accountAndIDQueryCondition, accountID, peer.ID).Save(peerCopy)\n\t\tif result.Error != nil {\n\t\t\treturn status.Errorf(status.Internal, \"failed to save peer to store: %v\", result.Error)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) UpdateAccountDomainAttributes(ctx context.Context, accountID string, domain string, category string, isPrimaryDomain bool) error {\n\taccountCopy := types.Account{\n\t\tDomain: domain,\n\t\tDomainCategory: category,\n\t\tIsDomainPrimaryAccount: isPrimaryDomain,\n\t}\n\n\tfieldsToUpdate := []string{\"domain\", \"domain_category\", \"is_domain_primary_account\"}\n\tresult := s.db.Model(&types.Account{}).\n\t\tSelect(fieldsToUpdate).\n\t\tWhere(idQueryCondition, accountID).\n\t\tUpdates(&accountCopy)\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"failed to update account domain attributes to store: %v\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"account %s\", accountID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) SavePeerStatus(ctx context.Context, accountID, peerID string, peerStatus nbpeer.PeerStatus) error {\n\tvar peerCopy nbpeer.Peer\n\tpeerCopy.Status = &peerStatus\n\n\tfieldsToUpdate := []string{\n\t\t\"peer_status_last_seen\", \"peer_status_connected\",\n\t\t\"peer_status_login_expired\", \"peer_status_required_approval\",\n\t}\n\tresult := s.db.Model(&nbpeer.Peer{}).\n\t\tSelect(fieldsToUpdate).\n\t\tWhere(accountAndIDQueryCondition, accountID, peerID).\n\t\tUpdates(&peerCopy)\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"failed to save peer status to store: %v\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, peerNotFoundFMT, peerID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) SavePeerLocation(ctx context.Context, accountID string, peerWithLocation *nbpeer.Peer) error {\n\t// To maintain data integrity, we create a copy of the peer's location to prevent unintended updates to other fields.\n\tvar peerCopy nbpeer.Peer\n\t// Since the location field has been migrated to JSON serialization,\n\t// updating the struct ensures the correct data format is inserted into the database.\n\tpeerCopy.Location = peerWithLocation.Location\n\n\tresult := s.db.Model(&nbpeer.Peer{}).\n\t\tWhere(accountAndIDQueryCondition, accountID, peerWithLocation.ID).\n\t\tUpdates(peerCopy)\n\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"failed to save peer locations to store: %v\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, peerNotFoundFMT, peerWithLocation.ID)\n\t}\n\n\treturn nil\n}\n\n// ApproveAccountPeers marks all peers that currently require approval in the given account as approved.\nfunc (s *SqlStore) ApproveAccountPeers(ctx context.Context, accountID string) (int, error) {\n\tresult := s.db.Model(&nbpeer.Peer{}).\n\t\tWhere(\"account_id = ? AND peer_status_requires_approval = ?\", accountID, true).\n\t\tUpdate(\"peer_status_requires_approval\", false)\n\tif result.Error != nil {\n\t\treturn 0, status.Errorf(status.Internal, \"failed to approve pending account peers: %v\", result.Error)\n\t}\n\n\treturn int(result.RowsAffected), nil\n}\n\n// SaveUsers saves the given list of users to the database.\nfunc (s *SqlStore) SaveUsers(ctx context.Context, users []*types.User) error {\n\tif len(users) == 0 {\n\t\treturn nil\n\t}\n\n\tusersCopy := make([]*types.User, len(users))\n\tfor i, user := range users {\n\t\tuserCopy := user.Copy()\n\t\tuserCopy.Email = user.Email\n\t\tuserCopy.Name = user.Name\n\t\tif err := userCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn fmt.Errorf(\"encrypt user: %w\", err)\n\t\t}\n\t\tusersCopy[i] = userCopy\n\t}\n\n\tresult := s.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&usersCopy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save users to store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save users to store\")\n\t}\n\treturn nil\n}\n\n// SaveUser saves the given user to the database.\nfunc (s *SqlStore) SaveUser(ctx context.Context, user *types.User) error {\n\tuserCopy := user.Copy()\n\tuserCopy.Email = user.Email\n\tuserCopy.Name = user.Name\n\n\tif err := userCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn fmt.Errorf(\"encrypt user: %w\", err)\n\t}\n\n\tresult := s.db.Save(userCopy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save user to store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save user to store\")\n\t}\n\treturn nil\n}\n\n// CreateGroups creates the given list of groups to the database.\nfunc (s *SqlStore) CreateGroups(ctx context.Context, accountID string, groups []*types.Group) error {\n\tif len(groups) == 0 {\n\t\treturn nil\n\t}\n\n\treturn s.db.Transaction(func(tx *gorm.DB) error {\n\t\tresult := tx.\n\t\t\tClauses(\n\t\t\t\tclause.OnConflict{\n\t\t\t\t\tWhere: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: \"groups.account_id\", Value: accountID}}},\n\t\t\t\t\tUpdateAll: true,\n\t\t\t\t},\n\t\t\t).\n\t\t\tOmit(clause.Associations).\n\t\t\tCreate(&groups)\n\t\tif result.Error != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"failed to save groups to store: %v\", result.Error)\n\t\t\treturn status.Errorf(status.Internal, \"failed to save groups to store\")\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\n// UpdateGroups updates the given list of groups to the database.\nfunc (s *SqlStore) UpdateGroups(ctx context.Context, accountID string, groups []*types.Group) error {\n\tif len(groups) == 0 {\n\t\treturn nil\n\t}\n\n\treturn s.db.Transaction(func(tx *gorm.DB) error {\n\t\tresult := tx.\n\t\t\tClauses(\n\t\t\t\tclause.OnConflict{\n\t\t\t\t\tWhere: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: \"groups.account_id\", Value: accountID}}},\n\t\t\t\t\tUpdateAll: true,\n\t\t\t\t},\n\t\t\t).\n\t\t\tOmit(clause.Associations).\n\t\t\tCreate(&groups)\n\t\tif result.Error != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"failed to save groups to store: %v\", result.Error)\n\t\t\treturn status.Errorf(status.Internal, \"failed to save groups to store\")\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\n// DeleteHashedPAT2TokenIDIndex is noop in SqlStore\nfunc (s *SqlStore) DeleteHashedPAT2TokenIDIndex(hashedToken string) error {\n\treturn nil\n}\n\n// DeleteTokenID2UserIDIndex is noop in SqlStore\nfunc (s *SqlStore) DeleteTokenID2UserIDIndex(tokenID string) error {\n\treturn nil\n}\n\nfunc (s *SqlStore) GetAccountByPrivateDomain(ctx context.Context, domain string) (*types.Account, error) {\n\taccountID, err := s.GetAccountIDByPrivateDomain(ctx, LockingStrengthNone, domain)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// TODO: rework to not call GetAccount\n\treturn s.GetAccount(ctx, accountID)\n}\n\nfunc (s *SqlStore) GetAccountIDByPrivateDomain(ctx context.Context, lockStrength LockingStrength, domain string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountID string\n\tresult := tx.Model(&types.Account{}).Select(\"id\").\n\t\tWhere(\"domain = ? and is_domain_primary_account = ? and domain_category = ?\",\n\t\t\tstrings.ToLower(domain), true, types.PrivateCategory,\n\t\t).Take(&accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: provided domain is not registered or is not private\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"error when getting account from the store: %s\", result.Error)\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn accountID, nil\n}\n\nfunc (s *SqlStore) GetAccountBySetupKey(ctx context.Context, setupKey string) (*types.Account, error) {\n\tvar key types.SetupKey\n\tresult := s.db.Select(\"account_id\").Take(&key, GetKeyQueryCondition(s), setupKey)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewSetupKeyNotFoundError(setupKey)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get account by setup key from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get account by setup key from store\")\n\t}\n\n\tif key.AccountID == \"\" {\n\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn s.GetAccount(ctx, key.AccountID)\n}\n\nfunc (s *SqlStore) GetTokenIDByHashedToken(ctx context.Context, hashedToken string) (string, error) {\n\tvar token types.PersonalAccessToken\n\tresult := s.db.Take(&token, \"hashed_token = ?\", hashedToken)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"error when getting token from the store: %s\", result.Error)\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn token.ID, nil\n}\n\nfunc (s *SqlStore) GetUserByPATID(ctx context.Context, lockStrength LockingStrength, patID string) (*types.User, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar user types.User\n\tresult := tx.\n\t\tJoins(\"JOIN personal_access_tokens ON personal_access_tokens.user_id = users.id\").\n\t\tWhere(\"personal_access_tokens.id = ?\", patID).Take(&user)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPATNotFoundError(patID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get token user from the store: %s\", result.Error)\n\t\treturn nil, status.NewGetUserFromStoreError()\n\t}\n\n\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t}\n\n\treturn &user, nil\n}\n\nfunc (s *SqlStore) GetUserByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (*types.User, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar user types.User\n\tresult := tx.Take(&user, idQueryCondition, userID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewUserNotFoundError(userID)\n\t\t}\n\t\treturn nil, status.NewGetUserFromStoreError()\n\t}\n\n\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t}\n\n\treturn &user, nil\n}\n\nfunc (s *SqlStore) DeleteUser(ctx context.Context, accountID, userID string) error {\n\terr := s.transaction(func(tx *gorm.DB) error {\n\t\tresult := tx.Delete(&types.PersonalAccessToken{}, \"user_id = ?\", userID)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\treturn tx.Delete(&types.User{}, accountAndIDQueryCondition, accountID, userID).Error\n\t})\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete user from the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to delete user from store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetAccountUsers(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.User, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar users []*types.User\n\tresult := tx.Find(&users, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"accountID not found: index lookup failed\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"error when getting users from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting users from store\")\n\t}\n\n\tfor _, user := range users {\n\t\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t\t}\n\t}\n\n\treturn users, nil\n}\n\nfunc (s *SqlStore) GetAccountOwner(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.User, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar user types.User\n\tresult := tx.Take(&user, \"account_id = ? AND role = ?\", accountID, types.UserRoleOwner)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"account owner not found: index lookup failed\")\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get account owner from the store\")\n\t}\n\n\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t}\n\n\treturn &user, nil\n}\n\n// SaveUserInvite saves a user invite to the database\nfunc (s *SqlStore) SaveUserInvite(ctx context.Context, invite *types.UserInviteRecord) error {\n\tinviteCopy := invite.Copy()\n\tif err := inviteCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn fmt.Errorf(\"encrypt invite: %w\", err)\n\t}\n\n\tresult := s.db.Save(inviteCopy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save user invite to store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save user invite to store\")\n\t}\n\treturn nil\n}\n\n// GetUserInviteByID retrieves a user invite by its ID and account ID\nfunc (s *SqlStore) GetUserInviteByID(ctx context.Context, lockStrength LockingStrength, accountID, inviteID string) (*types.UserInviteRecord, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar invite types.UserInviteRecord\n\tresult := tx.Where(\"account_id = ?\", accountID).Take(&invite, idQueryCondition, inviteID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"user invite not found\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get user invite from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get user invite from store\")\n\t}\n\n\tif err := invite.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt invite: %w\", err)\n\t}\n\n\treturn &invite, nil\n}\n\n// GetUserInviteByHashedToken retrieves a user invite by its hashed token\nfunc (s *SqlStore) GetUserInviteByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken string) (*types.UserInviteRecord, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar invite types.UserInviteRecord\n\tresult := tx.Take(&invite, \"hashed_token = ?\", hashedToken)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"user invite not found\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get user invite from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get user invite from store\")\n\t}\n\n\tif err := invite.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt invite: %w\", err)\n\t}\n\n\treturn &invite, nil\n}\n\n// GetUserInviteByEmail retrieves a user invite by account ID and email.\n// Since email is encrypted with random IVs, we fetch all invites for the account\n// and compare emails in memory after decryption.\nfunc (s *SqlStore) GetUserInviteByEmail(ctx context.Context, lockStrength LockingStrength, accountID, email string) (*types.UserInviteRecord, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar invites []*types.UserInviteRecord\n\tresult := tx.Find(&invites, \"account_id = ?\", accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get user invites from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get user invites from store\")\n\t}\n\n\tfor _, invite := range invites {\n\t\tif err := invite.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt invite: %w\", err)\n\t\t}\n\t\tif strings.EqualFold(invite.Email, email) {\n\t\t\treturn invite, nil\n\t\t}\n\t}\n\n\treturn nil, status.Errorf(status.NotFound, \"user invite not found for email\")\n}\n\n// GetAccountUserInvites retrieves all user invites for an account\nfunc (s *SqlStore) GetAccountUserInvites(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.UserInviteRecord, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar invites []*types.UserInviteRecord\n\tresult := tx.Find(&invites, \"account_id = ?\", accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get user invites from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get user invites from store\")\n\t}\n\n\tfor _, invite := range invites {\n\t\tif err := invite.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt invite: %w\", err)\n\t\t}\n\t}\n\n\treturn invites, nil\n}\n\n// DeleteUserInvite deletes a user invite by its ID\nfunc (s *SqlStore) DeleteUserInvite(ctx context.Context, inviteID string) error {\n\tresult := s.db.Delete(&types.UserInviteRecord{}, idQueryCondition, inviteID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete user invite from store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete user invite from store\")\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) GetAccountGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.Group, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar groups []*types.Group\n\tresult := tx.Preload(clause.Associations).Find(&groups, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"accountID not found: index lookup failed\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get account groups from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get account groups from the store\")\n\t}\n\n\tfor _, g := range groups {\n\t\tg.LoadGroupPeers()\n\t}\n\n\treturn groups, nil\n}\n\nfunc (s *SqlStore) GetResourceGroups(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) ([]*types.Group, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar groups []*types.Group\n\n\tlikePattern := `%\"ID\":\"` + resourceID + `\"%`\n\n\tresult := tx.\n\t\tPreload(clause.Associations).\n\t\tWhere(\"resources LIKE ?\", likePattern).\n\t\tFind(&groups)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, result.Error\n\t}\n\n\tfor _, g := range groups {\n\t\tg.LoadGroupPeers()\n\t}\n\n\treturn groups, nil\n}\n\nfunc (s *SqlStore) GetAccountsCounter(ctx context.Context) (int64, error) {\n\tvar count int64\n\tresult := s.db.Model(&types.Account{}).Count(&count)\n\tif result.Error != nil {\n\t\treturn 0, fmt.Errorf(\"failed to get all accounts counter: %w\", result.Error)\n\t}\n\n\treturn count, nil\n}\n\n// GetCustomDomainsCounts returns the total and validated custom domain counts.\nfunc (s *SqlStore) GetCustomDomainsCounts(ctx context.Context) (int64, int64, error) {\n\tvar total, validated int64\n\tif err := s.db.WithContext(ctx).Model(&domain.Domain{}).Count(&total).Error; err != nil {\n\t\treturn 0, 0, err\n\t}\n\tif err := s.db.WithContext(ctx).Model(&domain.Domain{}).Where(\"validated = ?\", true).Count(&validated).Error; err != nil {\n\t\treturn 0, 0, err\n\t}\n\treturn total, validated, nil\n}\n\nfunc (s *SqlStore) GetAllAccounts(ctx context.Context) (all []*types.Account) {\n\tvar accounts []types.Account\n\tresult := s.db.Find(&accounts)\n\tif result.Error != nil {\n\t\treturn all\n\t}\n\n\tfor _, account := range accounts {\n\t\tif acc, err := s.GetAccount(ctx, account.Id); err == nil {\n\t\t\tall = append(all, acc)\n\t\t}\n\t}\n\n\treturn all\n}\n\nfunc (s *SqlStore) GetAccountMeta(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.AccountMeta, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountMeta types.AccountMeta\n\tresult := tx.Model(&types.Account{}).\n\t\tTake(&accountMeta, idQueryCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"error when getting account meta %s from the store: %s\", accountID, result.Error)\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn &accountMeta, nil\n}\n\n// GetAccountOnboarding retrieves the onboarding information for a specific account.\nfunc (s *SqlStore) GetAccountOnboarding(ctx context.Context, accountID string) (*types.AccountOnboarding, error) {\n\tvar accountOnboarding types.AccountOnboarding\n\tresult := s.db.Model(&accountOnboarding).Take(&accountOnboarding, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewAccountOnboardingNotFoundError(accountID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"error when getting account onboarding %s from the store: %s\", accountID, result.Error)\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn &accountOnboarding, nil\n}\n\n// SaveAccountOnboarding updates the onboarding information for a specific account.\nfunc (s *SqlStore) SaveAccountOnboarding(ctx context.Context, onboarding *types.AccountOnboarding) error {\n\tresult := s.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(onboarding)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"error when saving account onboarding %s in the store: %s\", onboarding.AccountID, result.Error)\n\t\treturn status.Errorf(status.Internal, \"error when saving account onboarding %s in the store: %s\", onboarding.AccountID, result.Error)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetAccount(ctx context.Context, accountID string) (*types.Account, error) {\n\tif s.pool != nil {\n\t\treturn s.getAccountPgx(ctx, accountID)\n\t}\n\treturn s.getAccountGorm(ctx, accountID)\n}\n\nfunc (s *SqlStore) getAccountGorm(ctx context.Context, accountID string) (*types.Account, error) {\n\tstart := time.Now()\n\tdefer func() {\n\t\telapsed := time.Since(start)\n\t\tif elapsed > 1*time.Second {\n\t\t\tlog.WithContext(ctx).Tracef(\"GetAccount for account %s exceeded 1s, took: %v\", accountID, elapsed)\n\t\t}\n\t}()\n\n\tvar account types.Account\n\tresult := s.db.Model(&account).\n\t\tPreload(\"UsersG.PATsG\"). // have to be specified as this is nested reference\n\t\tPreload(\"Policies.Rules\").\n\t\tPreload(\"SetupKeysG\").\n\t\tPreload(\"PeersG\").\n\t\tPreload(\"UsersG\").\n\t\tPreload(\"GroupsG.GroupPeers\").\n\t\tPreload(\"RoutesG\").\n\t\tPreload(\"NameServerGroupsG\").\n\t\tPreload(\"PostureChecks\").\n\t\tPreload(\"Networks\").\n\t\tPreload(\"NetworkRouters\").\n\t\tPreload(\"NetworkResources\").\n\t\tPreload(\"Onboarding\").\n\t\tPreload(\"Services.Targets\").\n\t\tTake(&account, idQueryCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"error when getting account %s from the store: %s\", accountID, result.Error)\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\taccount.SetupKeys = make(map[string]*types.SetupKey, len(account.SetupKeysG))\n\tfor _, key := range account.SetupKeysG {\n\t\tif key.UpdatedAt.IsZero() {\n\t\t\tkey.UpdatedAt = key.CreatedAt\n\t\t}\n\t\tif key.AutoGroups == nil {\n\t\t\tkey.AutoGroups = []string{}\n\t\t}\n\t\taccount.SetupKeys[key.Key] = &key\n\t}\n\taccount.SetupKeysG = nil\n\n\taccount.Peers = make(map[string]*nbpeer.Peer, len(account.PeersG))\n\tfor _, peer := range account.PeersG {\n\t\taccount.Peers[peer.ID] = &peer\n\t}\n\taccount.PeersG = nil\n\taccount.Users = make(map[string]*types.User, len(account.UsersG))\n\tfor _, user := range account.UsersG {\n\t\tuser.PATs = make(map[string]*types.PersonalAccessToken, len(user.PATs))\n\t\tfor _, pat := range user.PATsG {\n\t\t\tpat.UserID = \"\"\n\t\t\tuser.PATs[pat.ID] = &pat\n\t\t}\n\t\tif user.AutoGroups == nil {\n\t\t\tuser.AutoGroups = []string{}\n\t\t}\n\t\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t\t}\n\t\taccount.Users[user.Id] = &user\n\t\tuser.PATsG = nil\n\t}\n\taccount.UsersG = nil\n\taccount.Groups = make(map[string]*types.Group, len(account.GroupsG))\n\tfor _, group := range account.GroupsG {\n\t\tgroup.Peers = make([]string, len(group.GroupPeers))\n\t\tfor i, gp := range group.GroupPeers {\n\t\t\tgroup.Peers[i] = gp.PeerID\n\t\t}\n\t\tif group.Resources == nil {\n\t\t\tgroup.Resources = []types.Resource{}\n\t\t}\n\t\taccount.Groups[group.ID] = group\n\t}\n\taccount.GroupsG = nil\n\n\taccount.Routes = make(map[route.ID]*route.Route, len(account.RoutesG))\n\tfor _, route := range account.RoutesG {\n\t\taccount.Routes[route.ID] = &route\n\t}\n\taccount.RoutesG = nil\n\taccount.NameServerGroups = make(map[string]*nbdns.NameServerGroup, len(account.NameServerGroupsG))\n\tfor _, ns := range account.NameServerGroupsG {\n\t\tns.AccountID = \"\"\n\t\tif ns.NameServers == nil {\n\t\t\tns.NameServers = []nbdns.NameServer{}\n\t\t}\n\t\tif ns.Groups == nil {\n\t\t\tns.Groups = []string{}\n\t\t}\n\t\tif ns.Domains == nil {\n\t\t\tns.Domains = []string{}\n\t\t}\n\t\taccount.NameServerGroups[ns.ID] = &ns\n\t}\n\taccount.NameServerGroupsG = nil\n\taccount.InitOnce()\n\treturn &account, nil\n}\n\nfunc (s *SqlStore) getAccountPgx(ctx context.Context, accountID string) (*types.Account, error) {\n\taccount, err := s.getAccount(ctx, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar wg sync.WaitGroup\n\terrChan := make(chan error, 12)\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tkeys, err := s.getSetupKeys(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.SetupKeysG = keys\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tpeers, err := s.getPeers(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.PeersG = peers\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tusers, err := s.getUsers(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.UsersG = users\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tgroups, err := s.getGroups(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.GroupsG = groups\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tpolicies, err := s.getPolicies(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.Policies = policies\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\troutes, err := s.getRoutes(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.RoutesG = routes\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tnsgs, err := s.getNameServerGroups(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.NameServerGroupsG = nsgs\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tchecks, err := s.getPostureChecks(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.PostureChecks = checks\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tservices, err := s.getServices(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.Services = services\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tnetworks, err := s.getNetworks(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.Networks = networks\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\trouters, err := s.getNetworkRouters(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.NetworkRouters = routers\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tresources, err := s.getNetworkResources(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.NetworkResources = resources\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\terr := s.getAccountOnboarding(ctx, accountID, account)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t}()\n\n\twg.Wait()\n\tclose(errChan)\n\tfor e := range errChan {\n\t\tif e != nil {\n\t\t\treturn nil, e\n\t\t}\n\t}\n\n\tvar userIDs []string\n\tfor _, u := range account.UsersG {\n\t\tuserIDs = append(userIDs, u.Id)\n\t}\n\tvar policyIDs []string\n\tfor _, p := range account.Policies {\n\t\tpolicyIDs = append(policyIDs, p.ID)\n\t}\n\tvar groupIDs []string\n\tfor _, g := range account.GroupsG {\n\t\tgroupIDs = append(groupIDs, g.ID)\n\t}\n\n\twg.Add(3)\n\terrChan = make(chan error, 3)\n\n\tvar pats []types.PersonalAccessToken\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tvar err error\n\t\tpats, err = s.getPersonalAccessTokens(ctx, userIDs)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t}\n\t}()\n\n\tvar rules []*types.PolicyRule\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tvar err error\n\t\trules, err = s.getPolicyRules(ctx, policyIDs)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t}\n\t}()\n\n\tvar groupPeers []types.GroupPeer\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tvar err error\n\t\tgroupPeers, err = s.getGroupPeers(ctx, groupIDs)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t}\n\t}()\n\n\twg.Wait()\n\tclose(errChan)\n\tfor e := range errChan {\n\t\tif e != nil {\n\t\t\treturn nil, e\n\t\t}\n\t}\n\n\tpatsByUserID := make(map[string][]*types.PersonalAccessToken)\n\tfor i := range pats {\n\t\tpat := &pats[i]\n\t\tpatsByUserID[pat.UserID] = append(patsByUserID[pat.UserID], pat)\n\t\tpat.UserID = \"\"\n\t}\n\n\trulesByPolicyID := make(map[string][]*types.PolicyRule)\n\tfor _, rule := range rules {\n\t\trulesByPolicyID[rule.PolicyID] = append(rulesByPolicyID[rule.PolicyID], rule)\n\t}\n\n\tpeersByGroupID := make(map[string][]string)\n\tfor _, gp := range groupPeers {\n\t\tpeersByGroupID[gp.GroupID] = append(peersByGroupID[gp.GroupID], gp.PeerID)\n\t}\n\n\taccount.SetupKeys = make(map[string]*types.SetupKey, len(account.SetupKeysG))\n\tfor i := range account.SetupKeysG {\n\t\tkey := &account.SetupKeysG[i]\n\t\taccount.SetupKeys[key.Key] = key\n\t}\n\n\taccount.Peers = make(map[string]*nbpeer.Peer, len(account.PeersG))\n\tfor i := range account.PeersG {\n\t\tpeer := &account.PeersG[i]\n\t\taccount.Peers[peer.ID] = peer\n\t}\n\n\taccount.Users = make(map[string]*types.User, len(account.UsersG))\n\tfor i := range account.UsersG {\n\t\tuser := &account.UsersG[i]\n\t\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t\t}\n\t\tuser.PATs = make(map[string]*types.PersonalAccessToken)\n\t\tif userPats, ok := patsByUserID[user.Id]; ok {\n\t\t\tfor j := range userPats {\n\t\t\t\tpat := userPats[j]\n\t\t\t\tuser.PATs[pat.ID] = pat\n\t\t\t}\n\t\t}\n\t\taccount.Users[user.Id] = user\n\t}\n\n\tfor i := range account.Policies {\n\t\tpolicy := account.Policies[i]\n\t\tif policyRules, ok := rulesByPolicyID[policy.ID]; ok {\n\t\t\tpolicy.Rules = policyRules\n\t\t}\n\t}\n\n\taccount.Groups = make(map[string]*types.Group, len(account.GroupsG))\n\tfor i := range account.GroupsG {\n\t\tgroup := account.GroupsG[i]\n\t\tif peerIDs, ok := peersByGroupID[group.ID]; ok {\n\t\t\tgroup.Peers = peerIDs\n\t\t}\n\t\taccount.Groups[group.ID] = group\n\t}\n\n\taccount.Routes = make(map[route.ID]*route.Route, len(account.RoutesG))\n\tfor i := range account.RoutesG {\n\t\troute := &account.RoutesG[i]\n\t\taccount.Routes[route.ID] = route\n\t}\n\n\taccount.NameServerGroups = make(map[string]*nbdns.NameServerGroup, len(account.NameServerGroupsG))\n\tfor i := range account.NameServerGroupsG {\n\t\tnsg := &account.NameServerGroupsG[i]\n\t\tnsg.AccountID = \"\"\n\t\taccount.NameServerGroups[nsg.ID] = nsg\n\t}\n\n\taccount.SetupKeysG = nil\n\taccount.PeersG = nil\n\taccount.UsersG = nil\n\taccount.GroupsG = nil\n\taccount.RoutesG = nil\n\taccount.NameServerGroupsG = nil\n\n\treturn account, nil\n}\n\nfunc (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Account, error) {\n\tvar account types.Account\n\taccount.Network = &types.Network{}\n\tconst accountQuery = `\n\t\tSELECT\n\t\t\tid, created_by, created_at, domain, domain_category, is_domain_primary_account,\n\t\t\t-- Embedded Network\n\t\t\tnetwork_identifier, network_net, network_dns, network_serial,\n\t\t\t-- Embedded DNSSettings\n\t\t\tdns_settings_disabled_management_groups,\n\t\t\t-- Embedded Settings\n\t\t\tsettings_peer_login_expiration_enabled, settings_peer_login_expiration,\n\t\t\tsettings_peer_inactivity_expiration_enabled, settings_peer_inactivity_expiration,\n\t\t\tsettings_regular_users_view_blocked, settings_groups_propagation_enabled,\n\t\t\tsettings_jwt_groups_enabled, settings_jwt_groups_claim_name, settings_jwt_allow_groups,\n\t\t\tsettings_routing_peer_dns_resolution_enabled, settings_dns_domain, settings_network_range,\n\t\t\tsettings_lazy_connection_enabled,\n\t\t\t-- Embedded ExtraSettings\n\t\t\tsettings_extra_peer_approval_enabled, settings_extra_user_approval_required,\n\t\t\tsettings_extra_integrated_validator, settings_extra_integrated_validator_groups\n\t\tFROM accounts WHERE id = $1`\n\n\tvar (\n\t\tsPeerLoginExpirationEnabled sql.NullBool\n\t\tsPeerLoginExpiration sql.NullInt64\n\t\tsPeerInactivityExpirationEnabled sql.NullBool\n\t\tsPeerInactivityExpiration sql.NullInt64\n\t\tsRegularUsersViewBlocked sql.NullBool\n\t\tsGroupsPropagationEnabled sql.NullBool\n\t\tsJWTGroupsEnabled sql.NullBool\n\t\tsJWTGroupsClaimName sql.NullString\n\t\tsJWTAllowGroups sql.NullString\n\t\tsRoutingPeerDNSResolutionEnabled sql.NullBool\n\t\tsDNSDomain sql.NullString\n\t\tsNetworkRange sql.NullString\n\t\tsLazyConnectionEnabled sql.NullBool\n\t\tsExtraPeerApprovalEnabled sql.NullBool\n\t\tsExtraUserApprovalRequired sql.NullBool\n\t\tsExtraIntegratedValidator sql.NullString\n\t\tsExtraIntegratedValidatorGroups sql.NullString\n\t\tnetworkNet sql.NullString\n\t\tdnsSettingsDisabledGroups sql.NullString\n\t\tnetworkIdentifier sql.NullString\n\t\tnetworkDns sql.NullString\n\t\tnetworkSerial sql.NullInt64\n\t\tcreatedAt sql.NullTime\n\t)\n\terr := s.pool.QueryRow(ctx, accountQuery, accountID).Scan(\n\t\t&account.Id, &account.CreatedBy, &createdAt, &account.Domain, &account.DomainCategory, &account.IsDomainPrimaryAccount,\n\t\t&networkIdentifier, &networkNet, &networkDns, &networkSerial,\n\t\t&dnsSettingsDisabledGroups,\n\t\t&sPeerLoginExpirationEnabled, &sPeerLoginExpiration,\n\t\t&sPeerInactivityExpirationEnabled, &sPeerInactivityExpiration,\n\t\t&sRegularUsersViewBlocked, &sGroupsPropagationEnabled,\n\t\t&sJWTGroupsEnabled, &sJWTGroupsClaimName, &sJWTAllowGroups,\n\t\t&sRoutingPeerDNSResolutionEnabled, &sDNSDomain, &sNetworkRange,\n\t\t&sLazyConnectionEnabled,\n\t\t&sExtraPeerApprovalEnabled, &sExtraUserApprovalRequired,\n\t\t&sExtraIntegratedValidator, &sExtraIntegratedValidatorGroups,\n\t)\n\tif err != nil {\n\t\tif errors.Is(err, pgx.ErrNoRows) {\n\t\t\treturn nil, status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(err)\n\t}\n\n\taccount.Settings = &types.Settings{Extra: &types.ExtraSettings{}}\n\tif networkNet.Valid {\n\t\t_ = json.Unmarshal([]byte(networkNet.String), &account.Network.Net)\n\t}\n\tif createdAt.Valid {\n\t\taccount.CreatedAt = createdAt.Time\n\t}\n\tif dnsSettingsDisabledGroups.Valid {\n\t\t_ = json.Unmarshal([]byte(dnsSettingsDisabledGroups.String), &account.DNSSettings.DisabledManagementGroups)\n\t}\n\tif networkIdentifier.Valid {\n\t\taccount.Network.Identifier = networkIdentifier.String\n\t}\n\tif networkDns.Valid {\n\t\taccount.Network.Dns = networkDns.String\n\t}\n\tif networkSerial.Valid {\n\t\taccount.Network.Serial = uint64(networkSerial.Int64)\n\t}\n\tif sPeerLoginExpirationEnabled.Valid {\n\t\taccount.Settings.PeerLoginExpirationEnabled = sPeerLoginExpirationEnabled.Bool\n\t}\n\tif sPeerLoginExpiration.Valid {\n\t\taccount.Settings.PeerLoginExpiration = time.Duration(sPeerLoginExpiration.Int64)\n\t}\n\tif sPeerInactivityExpirationEnabled.Valid {\n\t\taccount.Settings.PeerInactivityExpirationEnabled = sPeerInactivityExpirationEnabled.Bool\n\t}\n\tif sPeerInactivityExpiration.Valid {\n\t\taccount.Settings.PeerInactivityExpiration = time.Duration(sPeerInactivityExpiration.Int64)\n\t}\n\tif sRegularUsersViewBlocked.Valid {\n\t\taccount.Settings.RegularUsersViewBlocked = sRegularUsersViewBlocked.Bool\n\t}\n\tif sGroupsPropagationEnabled.Valid {\n\t\taccount.Settings.GroupsPropagationEnabled = sGroupsPropagationEnabled.Bool\n\t}\n\tif sJWTGroupsEnabled.Valid {\n\t\taccount.Settings.JWTGroupsEnabled = sJWTGroupsEnabled.Bool\n\t}\n\tif sJWTGroupsClaimName.Valid {\n\t\taccount.Settings.JWTGroupsClaimName = sJWTGroupsClaimName.String\n\t}\n\tif sRoutingPeerDNSResolutionEnabled.Valid {\n\t\taccount.Settings.RoutingPeerDNSResolutionEnabled = sRoutingPeerDNSResolutionEnabled.Bool\n\t}\n\tif sDNSDomain.Valid {\n\t\taccount.Settings.DNSDomain = sDNSDomain.String\n\t}\n\tif sLazyConnectionEnabled.Valid {\n\t\taccount.Settings.LazyConnectionEnabled = sLazyConnectionEnabled.Bool\n\t}\n\tif sJWTAllowGroups.Valid {\n\t\t_ = json.Unmarshal([]byte(sJWTAllowGroups.String), &account.Settings.JWTAllowGroups)\n\t}\n\tif sNetworkRange.Valid {\n\t\t_ = json.Unmarshal([]byte(sNetworkRange.String), &account.Settings.NetworkRange)\n\t}\n\n\tif sExtraPeerApprovalEnabled.Valid {\n\t\taccount.Settings.Extra.PeerApprovalEnabled = sExtraPeerApprovalEnabled.Bool\n\t}\n\tif sExtraUserApprovalRequired.Valid {\n\t\taccount.Settings.Extra.UserApprovalRequired = sExtraUserApprovalRequired.Bool\n\t}\n\tif sExtraIntegratedValidator.Valid {\n\t\taccount.Settings.Extra.IntegratedValidator = sExtraIntegratedValidator.String\n\t}\n\tif sExtraIntegratedValidatorGroups.Valid {\n\t\t_ = json.Unmarshal([]byte(sExtraIntegratedValidatorGroups.String), &account.Settings.Extra.IntegratedValidatorGroups)\n\t}\n\taccount.InitOnce()\n\treturn &account, nil\n}\n\nfunc (s *SqlStore) getSetupKeys(ctx context.Context, accountID string) ([]types.SetupKey, error) {\n\tconst query = `SELECT id, account_id, key, key_secret, name, type, created_at, expires_at, updated_at, \n\trevoked, used_times, last_used, auto_groups, usage_limit, ephemeral, allow_extra_dns_labels FROM setup_keys WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tkeys, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (types.SetupKey, error) {\n\t\tvar sk types.SetupKey\n\t\tvar autoGroups []byte\n\t\tvar skCreatedAt, expiresAt, updatedAt, lastUsed sql.NullTime\n\t\tvar revoked, ephemeral, allowExtraDNSLabels sql.NullBool\n\t\tvar usedTimes, usageLimit sql.NullInt64\n\n\t\terr := row.Scan(&sk.Id, &sk.AccountID, &sk.Key, &sk.KeySecret, &sk.Name, &sk.Type, &skCreatedAt,\n\t\t\t&expiresAt, &updatedAt, &revoked, &usedTimes, &lastUsed, &autoGroups, &usageLimit, &ephemeral, &allowExtraDNSLabels)\n\n\t\tif err == nil {\n\t\t\tif expiresAt.Valid {\n\t\t\t\tsk.ExpiresAt = &expiresAt.Time\n\t\t\t}\n\t\t\tif skCreatedAt.Valid {\n\t\t\t\tsk.CreatedAt = skCreatedAt.Time\n\t\t\t}\n\t\t\tif updatedAt.Valid {\n\t\t\t\tsk.UpdatedAt = updatedAt.Time\n\t\t\t\tif sk.UpdatedAt.IsZero() {\n\t\t\t\t\tsk.UpdatedAt = sk.CreatedAt\n\t\t\t\t}\n\t\t\t}\n\t\t\tif lastUsed.Valid {\n\t\t\t\tsk.LastUsed = &lastUsed.Time\n\t\t\t}\n\t\t\tif revoked.Valid {\n\t\t\t\tsk.Revoked = revoked.Bool\n\t\t\t}\n\t\t\tif usedTimes.Valid {\n\t\t\t\tsk.UsedTimes = int(usedTimes.Int64)\n\t\t\t}\n\t\t\tif usageLimit.Valid {\n\t\t\t\tsk.UsageLimit = int(usageLimit.Int64)\n\t\t\t}\n\t\t\tif ephemeral.Valid {\n\t\t\t\tsk.Ephemeral = ephemeral.Bool\n\t\t\t}\n\t\t\tif allowExtraDNSLabels.Valid {\n\t\t\t\tsk.AllowExtraDNSLabels = allowExtraDNSLabels.Bool\n\t\t\t}\n\t\t\tif autoGroups != nil {\n\t\t\t\t_ = json.Unmarshal(autoGroups, &sk.AutoGroups)\n\t\t\t} else {\n\t\t\t\tsk.AutoGroups = []string{}\n\t\t\t}\n\t\t}\n\t\treturn sk, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn keys, nil\n}\n\nfunc (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Peer, error) {\n\tconst query = `SELECT id, account_id, key, ip, name, dns_label, user_id, ssh_key, ssh_enabled, login_expiration_enabled,\n\tinactivity_expiration_enabled, last_login, created_at, ephemeral, extra_dns_labels, allow_extra_dns_labels, meta_hostname, \n\tmeta_go_os, meta_kernel, meta_core, meta_platform, meta_os, meta_os_version, meta_wt_version, meta_ui_version, \n\tmeta_kernel_version, meta_network_addresses, meta_system_serial_number, meta_system_product_name, meta_system_manufacturer,\n\tmeta_environment, meta_flags, meta_files, peer_status_last_seen, peer_status_connected, peer_status_login_expired, \n\tpeer_status_requires_approval, location_connection_ip, location_country_code, location_city_name, \n\tlocation_geo_name_id, proxy_meta_embedded, proxy_meta_cluster FROM peers WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpeers, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (nbpeer.Peer, error) {\n\t\tvar p nbpeer.Peer\n\t\tp.Status = &nbpeer.PeerStatus{}\n\t\tvar (\n\t\t\tlastLogin, createdAt sql.NullTime\n\t\t\tsshEnabled, loginExpirationEnabled, inactivityExpirationEnabled, ephemeral, allowExtraDNSLabels sql.NullBool\n\t\t\tpeerStatusLastSeen sql.NullTime\n\t\t\tpeerStatusConnected, peerStatusLoginExpired, peerStatusRequiresApproval, proxyEmbedded sql.NullBool\n\t\t\tip, extraDNS, netAddr, env, flags, files, connIP []byte\n\t\t\tmetaHostname, metaGoOS, metaKernel, metaCore, metaPlatform sql.NullString\n\t\t\tmetaOS, metaOSVersion, metaWtVersion, metaUIVersion, metaKernelVersion sql.NullString\n\t\t\tmetaSystemSerialNumber, metaSystemProductName, metaSystemManufacturer sql.NullString\n\t\t\tlocationCountryCode, locationCityName, proxyCluster sql.NullString\n\t\t\tlocationGeoNameID sql.NullInt64\n\t\t)\n\n\t\terr := row.Scan(&p.ID, &p.AccountID, &p.Key, &ip, &p.Name, &p.DNSLabel, &p.UserID, &p.SSHKey, &sshEnabled,\n\t\t\t&loginExpirationEnabled, &inactivityExpirationEnabled, &lastLogin, &createdAt, &ephemeral, &extraDNS,\n\t\t\t&allowExtraDNSLabels, &metaHostname, &metaGoOS, &metaKernel, &metaCore, &metaPlatform,\n\t\t\t&metaOS, &metaOSVersion, &metaWtVersion, &metaUIVersion, &metaKernelVersion, &netAddr,\n\t\t\t&metaSystemSerialNumber, &metaSystemProductName, &metaSystemManufacturer, &env, &flags, &files,\n\t\t\t&peerStatusLastSeen, &peerStatusConnected, &peerStatusLoginExpired, &peerStatusRequiresApproval, &connIP,\n\t\t\t&locationCountryCode, &locationCityName, &locationGeoNameID, &proxyEmbedded, &proxyCluster)\n\n\t\tif err == nil {\n\t\t\tif lastLogin.Valid {\n\t\t\t\tp.LastLogin = &lastLogin.Time\n\t\t\t}\n\t\t\tif createdAt.Valid {\n\t\t\t\tp.CreatedAt = createdAt.Time\n\t\t\t}\n\t\t\tif sshEnabled.Valid {\n\t\t\t\tp.SSHEnabled = sshEnabled.Bool\n\t\t\t}\n\t\t\tif loginExpirationEnabled.Valid {\n\t\t\t\tp.LoginExpirationEnabled = loginExpirationEnabled.Bool\n\t\t\t}\n\t\t\tif inactivityExpirationEnabled.Valid {\n\t\t\t\tp.InactivityExpirationEnabled = inactivityExpirationEnabled.Bool\n\t\t\t}\n\t\t\tif ephemeral.Valid {\n\t\t\t\tp.Ephemeral = ephemeral.Bool\n\t\t\t}\n\t\t\tif allowExtraDNSLabels.Valid {\n\t\t\t\tp.AllowExtraDNSLabels = allowExtraDNSLabels.Bool\n\t\t\t}\n\t\t\tif peerStatusLastSeen.Valid {\n\t\t\t\tp.Status.LastSeen = peerStatusLastSeen.Time\n\t\t\t}\n\t\t\tif peerStatusConnected.Valid {\n\t\t\t\tp.Status.Connected = peerStatusConnected.Bool\n\t\t\t}\n\t\t\tif peerStatusLoginExpired.Valid {\n\t\t\t\tp.Status.LoginExpired = peerStatusLoginExpired.Bool\n\t\t\t}\n\t\t\tif peerStatusRequiresApproval.Valid {\n\t\t\t\tp.Status.RequiresApproval = peerStatusRequiresApproval.Bool\n\t\t\t}\n\t\t\tif metaHostname.Valid {\n\t\t\t\tp.Meta.Hostname = metaHostname.String\n\t\t\t}\n\t\t\tif metaGoOS.Valid {\n\t\t\t\tp.Meta.GoOS = metaGoOS.String\n\t\t\t}\n\t\t\tif metaKernel.Valid {\n\t\t\t\tp.Meta.Kernel = metaKernel.String\n\t\t\t}\n\t\t\tif metaCore.Valid {\n\t\t\t\tp.Meta.Core = metaCore.String\n\t\t\t}\n\t\t\tif metaPlatform.Valid {\n\t\t\t\tp.Meta.Platform = metaPlatform.String\n\t\t\t}\n\t\t\tif metaOS.Valid {\n\t\t\t\tp.Meta.OS = metaOS.String\n\t\t\t}\n\t\t\tif metaOSVersion.Valid {\n\t\t\t\tp.Meta.OSVersion = metaOSVersion.String\n\t\t\t}\n\t\t\tif metaWtVersion.Valid {\n\t\t\t\tp.Meta.WtVersion = metaWtVersion.String\n\t\t\t}\n\t\t\tif metaUIVersion.Valid {\n\t\t\t\tp.Meta.UIVersion = metaUIVersion.String\n\t\t\t}\n\t\t\tif metaKernelVersion.Valid {\n\t\t\t\tp.Meta.KernelVersion = metaKernelVersion.String\n\t\t\t}\n\t\t\tif metaSystemSerialNumber.Valid {\n\t\t\t\tp.Meta.SystemSerialNumber = metaSystemSerialNumber.String\n\t\t\t}\n\t\t\tif metaSystemProductName.Valid {\n\t\t\t\tp.Meta.SystemProductName = metaSystemProductName.String\n\t\t\t}\n\t\t\tif metaSystemManufacturer.Valid {\n\t\t\t\tp.Meta.SystemManufacturer = metaSystemManufacturer.String\n\t\t\t}\n\t\t\tif locationCountryCode.Valid {\n\t\t\t\tp.Location.CountryCode = locationCountryCode.String\n\t\t\t}\n\t\t\tif locationCityName.Valid {\n\t\t\t\tp.Location.CityName = locationCityName.String\n\t\t\t}\n\t\t\tif locationGeoNameID.Valid {\n\t\t\t\tp.Location.GeoNameID = uint(locationGeoNameID.Int64)\n\t\t\t}\n\t\t\tif proxyEmbedded.Valid {\n\t\t\t\tp.ProxyMeta.Embedded = proxyEmbedded.Bool\n\t\t\t}\n\t\t\tif proxyCluster.Valid {\n\t\t\t\tp.ProxyMeta.Cluster = proxyCluster.String\n\t\t\t}\n\t\t\tif ip != nil {\n\t\t\t\t_ = json.Unmarshal(ip, &p.IP)\n\t\t\t}\n\t\t\tif extraDNS != nil {\n\t\t\t\t_ = json.Unmarshal(extraDNS, &p.ExtraDNSLabels)\n\t\t\t}\n\t\t\tif netAddr != nil {\n\t\t\t\t_ = json.Unmarshal(netAddr, &p.Meta.NetworkAddresses)\n\t\t\t}\n\t\t\tif env != nil {\n\t\t\t\t_ = json.Unmarshal(env, &p.Meta.Environment)\n\t\t\t}\n\t\t\tif flags != nil {\n\t\t\t\t_ = json.Unmarshal(flags, &p.Meta.Flags)\n\t\t\t}\n\t\t\tif files != nil {\n\t\t\t\t_ = json.Unmarshal(files, &p.Meta.Files)\n\t\t\t}\n\t\t\tif connIP != nil {\n\t\t\t\t_ = json.Unmarshal(connIP, &p.Location.ConnectionIP)\n\t\t\t}\n\t\t}\n\t\treturn p, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn peers, nil\n}\n\nfunc (s *SqlStore) getUsers(ctx context.Context, accountID string) ([]types.User, error) {\n\tconst query = `SELECT id, account_id, role, is_service_user, non_deletable, service_user_name, auto_groups, blocked, pending_approval, last_login, created_at, issued, integration_ref_id, integration_ref_integration_type, email, name FROM users WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tusers, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (types.User, error) {\n\t\tvar u types.User\n\t\tvar autoGroups []byte\n\t\tvar lastLogin, createdAt sql.NullTime\n\t\tvar isServiceUser, nonDeletable, blocked, pendingApproval sql.NullBool\n\t\terr := row.Scan(&u.Id, &u.AccountID, &u.Role, &isServiceUser, &nonDeletable, &u.ServiceUserName, &autoGroups, &blocked, &pendingApproval, &lastLogin, &createdAt, &u.Issued, &u.IntegrationReference.ID, &u.IntegrationReference.IntegrationType, &u.Email, &u.Name)\n\t\tif err == nil {\n\t\t\tif lastLogin.Valid {\n\t\t\t\tu.LastLogin = &lastLogin.Time\n\t\t\t}\n\t\t\tif createdAt.Valid {\n\t\t\t\tu.CreatedAt = createdAt.Time\n\t\t\t}\n\t\t\tif isServiceUser.Valid {\n\t\t\t\tu.IsServiceUser = isServiceUser.Bool\n\t\t\t}\n\t\t\tif nonDeletable.Valid {\n\t\t\t\tu.NonDeletable = nonDeletable.Bool\n\t\t\t}\n\t\t\tif blocked.Valid {\n\t\t\t\tu.Blocked = blocked.Bool\n\t\t\t}\n\t\t\tif pendingApproval.Valid {\n\t\t\t\tu.PendingApproval = pendingApproval.Bool\n\t\t\t}\n\t\t\tif autoGroups != nil {\n\t\t\t\t_ = json.Unmarshal(autoGroups, &u.AutoGroups)\n\t\t\t} else {\n\t\t\t\tu.AutoGroups = []string{}\n\t\t\t}\n\t\t}\n\t\treturn u, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn users, nil\n}\n\nfunc (s *SqlStore) getGroups(ctx context.Context, accountID string) ([]*types.Group, error) {\n\tconst query = `SELECT id, account_id, name, issued, resources, integration_ref_id, integration_ref_integration_type FROM groups WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroups, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (*types.Group, error) {\n\t\tvar g types.Group\n\t\tvar resources []byte\n\t\tvar refID sql.NullInt64\n\t\tvar refType sql.NullString\n\t\terr := row.Scan(&g.ID, &g.AccountID, &g.Name, &g.Issued, &resources, &refID, &refType)\n\t\tif err == nil {\n\t\t\tif refID.Valid {\n\t\t\t\tg.IntegrationReference.ID = int(refID.Int64)\n\t\t\t}\n\t\t\tif refType.Valid {\n\t\t\t\tg.IntegrationReference.IntegrationType = refType.String\n\t\t\t}\n\t\t\tif resources != nil {\n\t\t\t\t_ = json.Unmarshal(resources, &g.Resources)\n\t\t\t} else {\n\t\t\t\tg.Resources = []types.Resource{}\n\t\t\t}\n\t\t\tg.GroupPeers = []types.GroupPeer{}\n\t\t\tg.Peers = []string{}\n\t\t}\n\t\treturn &g, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn groups, nil\n}\n\nfunc (s *SqlStore) getPolicies(ctx context.Context, accountID string) ([]*types.Policy, error) {\n\tconst query = `SELECT id, account_id, name, description, enabled, source_posture_checks FROM policies WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpolicies, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (*types.Policy, error) {\n\t\tvar p types.Policy\n\t\tvar checks []byte\n\t\tvar enabled sql.NullBool\n\t\terr := row.Scan(&p.ID, &p.AccountID, &p.Name, &p.Description, &enabled, &checks)\n\t\tif err == nil {\n\t\t\tif enabled.Valid {\n\t\t\t\tp.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif checks != nil {\n\t\t\t\t_ = json.Unmarshal(checks, &p.SourcePostureChecks)\n\t\t\t}\n\t\t}\n\t\treturn &p, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn policies, nil\n}\n\nfunc (s *SqlStore) getRoutes(ctx context.Context, accountID string) ([]route.Route, error) {\n\tconst query = `SELECT id, account_id, network, domains, keep_route, net_id, description, peer, peer_groups, network_type, masquerade, metric, enabled, groups, access_control_groups, skip_auto_apply FROM routes WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\troutes, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (route.Route, error) {\n\t\tvar r route.Route\n\t\tvar network, domains, peerGroups, groups, accessGroups []byte\n\t\tvar keepRoute, masquerade, enabled, skipAutoApply sql.NullBool\n\t\tvar metric sql.NullInt64\n\t\terr := row.Scan(&r.ID, &r.AccountID, &network, &domains, &keepRoute, &r.NetID, &r.Description, &r.Peer, &peerGroups, &r.NetworkType, &masquerade, &metric, &enabled, &groups, &accessGroups, &skipAutoApply)\n\t\tif err == nil {\n\t\t\tif keepRoute.Valid {\n\t\t\t\tr.KeepRoute = keepRoute.Bool\n\t\t\t}\n\t\t\tif masquerade.Valid {\n\t\t\t\tr.Masquerade = masquerade.Bool\n\t\t\t}\n\t\t\tif enabled.Valid {\n\t\t\t\tr.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif skipAutoApply.Valid {\n\t\t\t\tr.SkipAutoApply = skipAutoApply.Bool\n\t\t\t}\n\t\t\tif metric.Valid {\n\t\t\t\tr.Metric = int(metric.Int64)\n\t\t\t}\n\t\t\tif network != nil {\n\t\t\t\t_ = json.Unmarshal(network, &r.Network)\n\t\t\t}\n\t\t\tif domains != nil {\n\t\t\t\t_ = json.Unmarshal(domains, &r.Domains)\n\t\t\t}\n\t\t\tif peerGroups != nil {\n\t\t\t\t_ = json.Unmarshal(peerGroups, &r.PeerGroups)\n\t\t\t}\n\t\t\tif groups != nil {\n\t\t\t\t_ = json.Unmarshal(groups, &r.Groups)\n\t\t\t}\n\t\t\tif accessGroups != nil {\n\t\t\t\t_ = json.Unmarshal(accessGroups, &r.AccessControlGroups)\n\t\t\t}\n\t\t}\n\t\treturn r, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn routes, nil\n}\n\nfunc (s *SqlStore) getNameServerGroups(ctx context.Context, accountID string) ([]nbdns.NameServerGroup, error) {\n\tconst query = `SELECT id, account_id, name, description, name_servers, groups, \"primary\", domains, enabled, search_domains_enabled FROM name_server_groups WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tnsgs, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (nbdns.NameServerGroup, error) {\n\t\tvar n nbdns.NameServerGroup\n\t\tvar ns, groups, domains []byte\n\t\tvar primary, enabled, searchDomainsEnabled sql.NullBool\n\t\terr := row.Scan(&n.ID, &n.AccountID, &n.Name, &n.Description, &ns, &groups, &primary, &domains, &enabled, &searchDomainsEnabled)\n\t\tif err == nil {\n\t\t\tif primary.Valid {\n\t\t\t\tn.Primary = primary.Bool\n\t\t\t}\n\t\t\tif enabled.Valid {\n\t\t\t\tn.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif searchDomainsEnabled.Valid {\n\t\t\t\tn.SearchDomainsEnabled = searchDomainsEnabled.Bool\n\t\t\t}\n\t\t\tif ns != nil {\n\t\t\t\t_ = json.Unmarshal(ns, &n.NameServers)\n\t\t\t} else {\n\t\t\t\tn.NameServers = []nbdns.NameServer{}\n\t\t\t}\n\t\t\tif groups != nil {\n\t\t\t\t_ = json.Unmarshal(groups, &n.Groups)\n\t\t\t} else {\n\t\t\t\tn.Groups = []string{}\n\t\t\t}\n\t\t\tif domains != nil {\n\t\t\t\t_ = json.Unmarshal(domains, &n.Domains)\n\t\t\t} else {\n\t\t\t\tn.Domains = []string{}\n\t\t\t}\n\t\t}\n\t\treturn n, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn nsgs, nil\n}\n\nfunc (s *SqlStore) getPostureChecks(ctx context.Context, accountID string) ([]*posture.Checks, error) {\n\tconst query = `SELECT id, account_id, name, description, checks FROM posture_checks WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tchecks, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (*posture.Checks, error) {\n\t\tvar c posture.Checks\n\t\tvar checksDef []byte\n\t\terr := row.Scan(&c.ID, &c.AccountID, &c.Name, &c.Description, &checksDef)\n\t\tif err == nil && checksDef != nil {\n\t\t\t_ = json.Unmarshal(checksDef, &c.Checks)\n\t\t}\n\t\treturn &c, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn checks, nil\n}\n\nfunc (s *SqlStore) getServices(ctx context.Context, accountID string) ([]*reverseproxy.Service, error) {\n\tconst serviceQuery = `SELECT id, account_id, name, domain, enabled, auth,\n\t\tmeta_created_at, meta_certificate_issued_at, meta_status, proxy_cluster,\n\t\tpass_host_header, rewrite_redirects, session_private_key, session_public_key\n\t\tFROM services WHERE account_id = $1`\n\n\tconst targetsQuery = `SELECT id, account_id, service_id, path, host, port, protocol,\n\t\ttarget_id, target_type, enabled\n\t\tFROM targets WHERE service_id = ANY($1)`\n\n\tserviceRows, err := s.pool.Query(ctx, serviceQuery, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tservices, err := pgx.CollectRows(serviceRows, func(row pgx.CollectableRow) (*reverseproxy.Service, error) {\n\t\tvar s reverseproxy.Service\n\t\tvar auth []byte\n\t\tvar createdAt, certIssuedAt sql.NullTime\n\t\tvar status, proxyCluster, sessionPrivateKey, sessionPublicKey sql.NullString\n\t\terr := row.Scan(\n\t\t\t&s.ID,\n\t\t\t&s.AccountID,\n\t\t\t&s.Name,\n\t\t\t&s.Domain,\n\t\t\t&s.Enabled,\n\t\t\t&auth,\n\t\t\t&createdAt,\n\t\t\t&certIssuedAt,\n\t\t\t&status,\n\t\t\t&proxyCluster,\n\t\t\t&s.PassHostHeader,\n\t\t\t&s.RewriteRedirects,\n\t\t\t&sessionPrivateKey,\n\t\t\t&sessionPublicKey,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif auth != nil {\n\t\t\tif err := json.Unmarshal(auth, &s.Auth); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\ts.Meta = reverseproxy.ServiceMeta{}\n\t\tif createdAt.Valid {\n\t\t\ts.Meta.CreatedAt = createdAt.Time\n\t\t}\n\t\tif certIssuedAt.Valid {\n\t\t\tt := certIssuedAt.Time\n\t\t\ts.Meta.CertificateIssuedAt = &t\n\t\t}\n\t\tif status.Valid {\n\t\t\ts.Meta.Status = status.String\n\t\t}\n\t\tif proxyCluster.Valid {\n\t\t\ts.ProxyCluster = proxyCluster.String\n\t\t}\n\t\tif sessionPrivateKey.Valid {\n\t\t\ts.SessionPrivateKey = sessionPrivateKey.String\n\t\t}\n\t\tif sessionPublicKey.Valid {\n\t\t\ts.SessionPublicKey = sessionPublicKey.String\n\t\t}\n\n\t\ts.Targets = []*reverseproxy.Target{}\n\t\treturn &s, nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(services) == 0 {\n\t\treturn services, nil\n\t}\n\n\tserviceIDs := make([]string, len(services))\n\tserviceMap := make(map[string]*reverseproxy.Service)\n\tfor i, s := range services {\n\t\tserviceIDs[i] = s.ID\n\t\tserviceMap[s.ID] = s\n\t}\n\n\ttargetRows, err := s.pool.Query(ctx, targetsQuery, serviceIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttargets, err := pgx.CollectRows(targetRows, func(row pgx.CollectableRow) (*reverseproxy.Target, error) {\n\t\tvar t reverseproxy.Target\n\t\tvar path sql.NullString\n\t\terr := row.Scan(\n\t\t\t&t.ID,\n\t\t\t&t.AccountID,\n\t\t\t&t.ServiceID,\n\t\t\t&path,\n\t\t\t&t.Host,\n\t\t\t&t.Port,\n\t\t\t&t.Protocol,\n\t\t\t&t.TargetId,\n\t\t\t&t.TargetType,\n\t\t\t&t.Enabled,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif path.Valid {\n\t\t\tt.Path = &path.String\n\t\t}\n\t\treturn &t, nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, target := range targets {\n\t\tif service, ok := serviceMap[target.ServiceID]; ok {\n\t\t\tservice.Targets = append(service.Targets, target)\n\t\t}\n\t}\n\n\treturn services, nil\n}\n\nfunc (s *SqlStore) getNetworks(ctx context.Context, accountID string) ([]*networkTypes.Network, error) {\n\tconst query = `SELECT id, account_id, name, description FROM networks WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tnetworks, err := pgx.CollectRows(rows, pgx.RowToStructByName[networkTypes.Network])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult := make([]*networkTypes.Network, len(networks))\n\tfor i := range networks {\n\t\tresult[i] = &networks[i]\n\t}\n\treturn result, nil\n}\n\nfunc (s *SqlStore) getNetworkRouters(ctx context.Context, accountID string) ([]*routerTypes.NetworkRouter, error) {\n\tconst query = `SELECT id, network_id, account_id, peer, peer_groups, masquerade, metric, enabled FROM network_routers WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trouters, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (routerTypes.NetworkRouter, error) {\n\t\tvar r routerTypes.NetworkRouter\n\t\tvar peerGroups []byte\n\t\tvar masquerade, enabled sql.NullBool\n\t\tvar metric sql.NullInt64\n\t\terr := row.Scan(&r.ID, &r.NetworkID, &r.AccountID, &r.Peer, &peerGroups, &masquerade, &metric, &enabled)\n\t\tif err == nil {\n\t\t\tif masquerade.Valid {\n\t\t\t\tr.Masquerade = masquerade.Bool\n\t\t\t}\n\t\t\tif enabled.Valid {\n\t\t\t\tr.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif metric.Valid {\n\t\t\t\tr.Metric = int(metric.Int64)\n\t\t\t}\n\t\t\tif peerGroups != nil {\n\t\t\t\t_ = json.Unmarshal(peerGroups, &r.PeerGroups)\n\t\t\t}\n\t\t}\n\t\treturn r, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult := make([]*routerTypes.NetworkRouter, len(routers))\n\tfor i := range routers {\n\t\tresult[i] = &routers[i]\n\t}\n\treturn result, nil\n}\n\nfunc (s *SqlStore) getNetworkResources(ctx context.Context, accountID string) ([]*resourceTypes.NetworkResource, error) {\n\tconst query = `SELECT id, network_id, account_id, name, description, type, domain, prefix, enabled FROM network_resources WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresources, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (resourceTypes.NetworkResource, error) {\n\t\tvar r resourceTypes.NetworkResource\n\t\tvar prefix []byte\n\t\tvar enabled sql.NullBool\n\t\terr := row.Scan(&r.ID, &r.NetworkID, &r.AccountID, &r.Name, &r.Description, &r.Type, &r.Domain, &prefix, &enabled)\n\t\tif err == nil {\n\t\t\tif enabled.Valid {\n\t\t\t\tr.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif prefix != nil {\n\t\t\t\t_ = json.Unmarshal(prefix, &r.Prefix)\n\t\t\t}\n\t\t}\n\t\treturn r, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult := make([]*resourceTypes.NetworkResource, len(resources))\n\tfor i := range resources {\n\t\tresult[i] = &resources[i]\n\t}\n\treturn result, nil\n}\n\nfunc (s *SqlStore) getAccountOnboarding(ctx context.Context, accountID string, account *types.Account) error {\n\tconst query = `SELECT account_id, onboarding_flow_pending, signup_form_pending, created_at, updated_at FROM account_onboardings WHERE account_id = $1`\n\tvar onboardingFlowPending, signupFormPending sql.NullBool\n\tvar createdAt, updatedAt sql.NullTime\n\terr := s.pool.QueryRow(ctx, query, accountID).Scan(\n\t\t&account.Onboarding.AccountID,\n\t\t&onboardingFlowPending,\n\t\t&signupFormPending,\n\t\t&createdAt,\n\t\t&updatedAt,\n\t)\n\tif err != nil && !errors.Is(err, pgx.ErrNoRows) {\n\t\treturn err\n\t}\n\tif createdAt.Valid {\n\t\taccount.Onboarding.CreatedAt = createdAt.Time\n\t}\n\tif updatedAt.Valid {\n\t\taccount.Onboarding.UpdatedAt = updatedAt.Time\n\t}\n\tif onboardingFlowPending.Valid {\n\t\taccount.Onboarding.OnboardingFlowPending = onboardingFlowPending.Bool\n\t}\n\tif signupFormPending.Valid {\n\t\taccount.Onboarding.SignupFormPending = signupFormPending.Bool\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) getPersonalAccessTokens(ctx context.Context, userIDs []string) ([]types.PersonalAccessToken, error) {\n\tif len(userIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\tconst query = `SELECT id, user_id, name, hashed_token, expiration_date, created_by, created_at, last_used FROM personal_access_tokens WHERE user_id = ANY($1)`\n\trows, err := s.pool.Query(ctx, query, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpats, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (types.PersonalAccessToken, error) {\n\t\tvar pat types.PersonalAccessToken\n\t\tvar expirationDate, lastUsed, createdAt sql.NullTime\n\t\terr := row.Scan(&pat.ID, &pat.UserID, &pat.Name, &pat.HashedToken, &expirationDate, &pat.CreatedBy, &createdAt, &lastUsed)\n\t\tif err == nil {\n\t\t\tif expirationDate.Valid {\n\t\t\t\tpat.ExpirationDate = &expirationDate.Time\n\t\t\t}\n\t\t\tif createdAt.Valid {\n\t\t\t\tpat.CreatedAt = createdAt.Time\n\t\t\t}\n\t\t\tif lastUsed.Valid {\n\t\t\t\tpat.LastUsed = &lastUsed.Time\n\t\t\t}\n\t\t}\n\t\treturn pat, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn pats, nil\n}\n\nfunc (s *SqlStore) getPolicyRules(ctx context.Context, policyIDs []string) ([]*types.PolicyRule, error) {\n\tif len(policyIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\tconst query = `SELECT id, policy_id, name, description, enabled, action, destinations, destination_resource, sources, source_resource, bidirectional, protocol, ports, port_ranges, authorized_groups, authorized_user FROM policy_rules WHERE policy_id = ANY($1)`\n\trows, err := s.pool.Query(ctx, query, policyIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trules, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (*types.PolicyRule, error) {\n\t\tvar r types.PolicyRule\n\t\tvar dest, destRes, sources, sourceRes, ports, portRanges, authorizedGroups []byte\n\t\tvar enabled, bidirectional sql.NullBool\n\t\tvar authorizedUser sql.NullString\n\t\terr := row.Scan(&r.ID, &r.PolicyID, &r.Name, &r.Description, &enabled, &r.Action, &dest, &destRes, &sources, &sourceRes, &bidirectional, &r.Protocol, &ports, &portRanges, &authorizedGroups, &authorizedUser)\n\t\tif err == nil {\n\t\t\tif enabled.Valid {\n\t\t\t\tr.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif bidirectional.Valid {\n\t\t\t\tr.Bidirectional = bidirectional.Bool\n\t\t\t}\n\t\t\tif dest != nil {\n\t\t\t\t_ = json.Unmarshal(dest, &r.Destinations)\n\t\t\t}\n\t\t\tif destRes != nil {\n\t\t\t\t_ = json.Unmarshal(destRes, &r.DestinationResource)\n\t\t\t}\n\t\t\tif sources != nil {\n\t\t\t\t_ = json.Unmarshal(sources, &r.Sources)\n\t\t\t}\n\t\t\tif sourceRes != nil {\n\t\t\t\t_ = json.Unmarshal(sourceRes, &r.SourceResource)\n\t\t\t}\n\t\t\tif ports != nil {\n\t\t\t\t_ = json.Unmarshal(ports, &r.Ports)\n\t\t\t}\n\t\t\tif portRanges != nil {\n\t\t\t\t_ = json.Unmarshal(portRanges, &r.PortRanges)\n\t\t\t}\n\t\t\tif authorizedGroups != nil {\n\t\t\t\t_ = json.Unmarshal(authorizedGroups, &r.AuthorizedGroups)\n\t\t\t}\n\t\t\tif authorizedUser.Valid {\n\t\t\t\tr.AuthorizedUser = authorizedUser.String\n\t\t\t}\n\t\t}\n\t\treturn &r, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn rules, nil\n}\n\nfunc (s *SqlStore) getGroupPeers(ctx context.Context, groupIDs []string) ([]types.GroupPeer, error) {\n\tif len(groupIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\tconst query = `SELECT account_id, group_id, peer_id FROM group_peers WHERE group_id = ANY($1)`\n\trows, err := s.pool.Query(ctx, query, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroupPeers, err := pgx.CollectRows(rows, pgx.RowToStructByName[types.GroupPeer])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn groupPeers, nil\n}\n\nfunc (s *SqlStore) GetAccountByUser(ctx context.Context, userID string) (*types.Account, error) {\n\tvar user types.User\n\tresult := s.db.Select(\"account_id\").Take(&user, idQueryCondition, userID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\tif user.AccountID == \"\" {\n\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn s.GetAccount(ctx, user.AccountID)\n}\n\nfunc (s *SqlStore) GetAccountByPeerID(ctx context.Context, peerID string) (*types.Account, error) {\n\tvar peer nbpeer.Peer\n\tresult := s.db.Select(\"account_id\").Take(&peer, idQueryCondition, peerID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\tif peer.AccountID == \"\" {\n\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn s.GetAccount(ctx, peer.AccountID)\n}\n\nfunc (s *SqlStore) GetAccountByPeerPubKey(ctx context.Context, peerKey string) (*types.Account, error) {\n\tvar peer nbpeer.Peer\n\tresult := s.db.Select(\"account_id\").Take(&peer, GetKeyQueryCondition(s), peerKey)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\tif peer.AccountID == \"\" {\n\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn s.GetAccount(ctx, peer.AccountID)\n}\n\nfunc (s *SqlStore) GetAnyAccountID(ctx context.Context) (string, error) {\n\tvar account types.Account\n\tresult := s.db.Select(\"id\").Order(\"created_at desc\").Limit(1).Find(&account)\n\tif result.Error != nil {\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\tif result.RowsAffected == 0 {\n\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn account.Id, nil\n}\n\nfunc (s *SqlStore) GetAccountIDByPeerPubKey(ctx context.Context, peerKey string) (string, error) {\n\tvar peer nbpeer.Peer\n\tvar accountID string\n\tresult := s.db.Model(&peer).Select(\"account_id\").Where(GetKeyQueryCondition(s), peerKey).Take(&accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn accountID, nil\n}\n\nfunc (s *SqlStore) GetAccountIDByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountID string\n\tresult := tx.Model(&types.User{}).\n\t\tSelect(\"account_id\").Where(idQueryCondition, userID).Take(&accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn accountID, nil\n}\n\nfunc (s *SqlStore) GetAccountIDByPeerID(ctx context.Context, lockStrength LockingStrength, peerID string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountID string\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tSelect(\"account_id\").Where(idQueryCondition, peerID).Take(&accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"peer %s account not found\", peerID)\n\t\t}\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn accountID, nil\n}\n\nfunc (s *SqlStore) GetAccountIDBySetupKey(ctx context.Context, setupKey string) (string, error) {\n\tvar accountID string\n\tresult := s.db.Model(&types.SetupKey{}).Select(\"account_id\").Where(GetKeyQueryCondition(s), setupKey).Take(&accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.NewSetupKeyNotFoundError(setupKey)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get account ID by setup key from store: %v\", result.Error)\n\t\treturn \"\", status.Errorf(status.Internal, \"failed to get account ID by setup key from store\")\n\t}\n\n\tif accountID == \"\" {\n\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn accountID, nil\n}\n\nfunc (s *SqlStore) GetTakenIPs(ctx context.Context, lockStrength LockingStrength, accountID string) ([]net.IP, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar ipJSONStrings []string\n\n\t// Fetch the IP addresses as JSON strings\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tWhere(\"account_id = ?\", accountID).\n\t\tPluck(\"ip\", &ipJSONStrings)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"no peers found for the account\")\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting IPs from store: %s\", result.Error)\n\t}\n\n\t// Convert the JSON strings to net.IP objects\n\tips := make([]net.IP, len(ipJSONStrings))\n\tfor i, ipJSON := range ipJSONStrings {\n\t\tvar ip net.IP\n\t\tif err := json.Unmarshal([]byte(ipJSON), &ip); err != nil {\n\t\t\treturn nil, status.Errorf(status.Internal, \"issue parsing IP JSON from store\")\n\t\t}\n\t\tips[i] = ip\n\t}\n\n\treturn ips, nil\n}\n\nfunc (s *SqlStore) GetPeerLabelsInAccount(ctx context.Context, lockStrength LockingStrength, accountID string, dnsLabel string) ([]string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar labels []string\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tWhere(\"account_id = ? AND dns_label LIKE ?\", accountID, dnsLabel+\"%\").\n\t\tPluck(\"dns_label\", &labels)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"no peers found for the account\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"error when getting dns labels from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting dns labels from store: %s\", result.Error)\n\t}\n\n\treturn labels, nil\n}\n\nfunc (s *SqlStore) GetAccountNetwork(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.Network, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountNetwork types.AccountNetwork\n\tif err := tx.Model(&types.Account{}).Where(idQueryCondition, accountID).Take(&accountNetwork).Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting network from store: %s\", err)\n\t}\n\treturn accountNetwork.Network, nil\n}\n\nfunc (s *SqlStore) GetPeerByPeerPubKey(ctx context.Context, lockStrength LockingStrength, peerKey string) (*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peer nbpeer.Peer\n\tresult := tx.Take(&peer, GetKeyQueryCondition(s), peerKey)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPeerNotFoundError(peerKey)\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting peer from store: %s\", result.Error)\n\t}\n\n\treturn &peer, nil\n}\n\nfunc (s *SqlStore) GetAccountSettings(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.Settings, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountSettings types.AccountSettings\n\tif err := tx.Model(&types.Account{}).Where(idQueryCondition, accountID).Take(&accountSettings).Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"settings not found\")\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting settings from store: %s\", err)\n\t}\n\treturn accountSettings.Settings, nil\n}\n\nfunc (s *SqlStore) GetAccountCreatedBy(ctx context.Context, lockStrength LockingStrength, accountID string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar createdBy string\n\tresult := tx.Model(&types.Account{}).\n\t\tSelect(\"created_by\").Take(&createdBy, idQueryCondition, accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn createdBy, nil\n}\n\n// SaveUserLastLogin stores the last login time for a user in DB.\nfunc (s *SqlStore) SaveUserLastLogin(ctx context.Context, accountID, userID string, lastLogin time.Time) error {\n\tvar user types.User\n\tresult := s.db.Take(&user, accountAndIDQueryCondition, accountID, userID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn status.NewUserNotFoundError(userID)\n\t\t}\n\t\treturn status.NewGetUserFromStoreError()\n\t}\n\n\tif !lastLogin.IsZero() {\n\t\tuser.LastLogin = &lastLogin\n\t\treturn s.db.Save(&user).Error\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetPostureCheckByChecksDefinition(accountID string, checks *posture.ChecksDefinition) (*posture.Checks, error) {\n\tdefinitionJSON, err := json.Marshal(checks)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar postureCheck posture.Checks\n\terr = s.db.Where(\"account_id = ? AND checks = ?\", accountID, string(definitionJSON)).Take(&postureCheck).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &postureCheck, nil\n}\n\n// Close closes the underlying DB connection\nfunc (s *SqlStore) Close(_ context.Context) error {\n\tsql, err := s.db.DB()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get db: %w\", err)\n\t}\n\treturn sql.Close()\n}\n\n// GetStoreEngine returns underlying store engine\nfunc (s *SqlStore) GetStoreEngine() types.Engine {\n\treturn s.storeEngine\n}\n\n// NewSqliteStore creates a new SQLite store.\nfunc NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tstoreStr := fmt.Sprintf(\"%s?cache=shared\", storeSqliteFileName)\n\tif runtime.GOOS == \"windows\" {\n\t\t// Vo avoid `The process cannot access the file because it is being used by another process` on Windows\n\t\tstoreStr = storeSqliteFileName\n\t}\n\n\tfile := filepath.Join(dataDir, storeStr)\n\tdb, err := gorm.Open(sqlite.Open(file), getGormConfig())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn NewSqlStore(ctx, db, types.SqliteStoreEngine, metrics, skipMigration)\n}\n\n// NewPostgresqlStore creates a new Postgres store.\nfunc NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tdb, err := gorm.Open(postgres.Open(dsn), getGormConfig())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpool, err := connectToPgDb(context.Background(), dsn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tstore, err := NewSqlStore(ctx, db, types.PostgresStoreEngine, metrics, skipMigration)\n\tif err != nil {\n\t\tpool.Close()\n\t\treturn nil, err\n\t}\n\tstore.pool = pool\n\treturn store, nil\n}\n\nfunc connectToPgDb(ctx context.Context, dsn string) (*pgxpool.Pool, error) {\n\tconfig, err := pgxpool.ParseConfig(dsn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse database config: %w\", err)\n\t}\n\n\tconfig.MaxConns = pgMaxConnections\n\tconfig.MinConns = pgMinConnections\n\tconfig.MaxConnLifetime = pgMaxConnLifetime\n\tconfig.HealthCheckPeriod = pgHealthCheckPeriod\n\n\tpool, err := pgxpool.NewWithConfig(ctx, config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create connection pool: %w\", err)\n\t}\n\n\tif err := pool.Ping(ctx); err != nil {\n\t\tpool.Close()\n\t\treturn nil, fmt.Errorf(\"unable to ping database: %w\", err)\n\t}\n\n\treturn pool, nil\n}\n\n// NewMysqlStore creates a new MySQL store.\nfunc NewMysqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tdb, err := gorm.Open(mysql.Open(dsn+\"?charset=utf8&parseTime=True&loc=Local\"), getGormConfig())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn NewSqlStore(ctx, db, types.MysqlStoreEngine, metrics, skipMigration)\n}\n\nfunc getGormConfig() *gorm.Config {\n\treturn &gorm.Config{\n\t\tLogger: logger.Default.LogMode(logger.Silent),\n\t\tCreateBatchSize: 400,\n\t}\n}\n\n// newPostgresStore initializes a new Postgres store.\nfunc newPostgresStore(ctx context.Context, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {\n\tdsn, ok := lookupDSNEnv(postgresDsnEnv, postgresDsnEnvLegacy)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"%s is not set\", postgresDsnEnv)\n\t}\n\treturn NewPostgresqlStore(ctx, dsn, metrics, skipMigration)\n}\n\n// newMysqlStore initializes a new MySQL store.\nfunc newMysqlStore(ctx context.Context, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {\n\tdsn, ok := lookupDSNEnv(mysqlDsnEnv, mysqlDsnEnvLegacy)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"%s is not set\", mysqlDsnEnv)\n\t}\n\treturn NewMysqlStore(ctx, dsn, metrics, skipMigration)\n}\n\n// NewSqliteStoreFromFileStore restores a store from FileStore and stores SQLite DB in the file located in datadir.\nfunc NewSqliteStoreFromFileStore(ctx context.Context, fileStore *FileStore, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tstore, err := NewSqliteStore(ctx, dataDir, metrics, skipMigration)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = store.SaveInstallationID(ctx, fileStore.InstallationID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, account := range fileStore.GetAllAccounts(ctx) {\n\t\t_, err = account.GetGroupAll()\n\t\tif err != nil {\n\t\t\tif err := account.AddAllGroup(false); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\terr := store.SaveAccount(ctx, account)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn store, nil\n}\n\n// NewPostgresqlStoreFromSqlStore restores a store from SqlStore and stores Postgres DB.\nfunc NewPostgresqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) {\n\tstore, err := NewPostgresqlStoreForTests(ctx, dsn, metrics, false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = store.SaveInstallationID(ctx, sqliteStore.GetInstallationID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, account := range sqliteStore.GetAllAccounts(ctx) {\n\t\terr := store.SaveAccount(ctx, account)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn store, nil\n}\n\n// used for tests only\nfunc NewPostgresqlStoreForTests(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tdb, err := gorm.Open(postgres.Open(dsn), getGormConfig())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpool, err := connectToPgDbForTests(context.Background(), dsn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tstore, err := NewSqlStore(ctx, db, types.PostgresStoreEngine, metrics, skipMigration)\n\tif err != nil {\n\t\tpool.Close()\n\t\treturn nil, err\n\t}\n\tstore.pool = pool\n\treturn store, nil\n}\n\n// used for tests only\nfunc connectToPgDbForTests(ctx context.Context, dsn string) (*pgxpool.Pool, error) {\n\tconfig, err := pgxpool.ParseConfig(dsn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse database config: %w\", err)\n\t}\n\n\tconfig.MaxConns = 5\n\tconfig.MinConns = 1\n\tconfig.MaxConnLifetime = 30 * time.Second\n\tconfig.HealthCheckPeriod = 10 * time.Second\n\n\tpool, err := pgxpool.NewWithConfig(ctx, config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create connection pool: %w\", err)\n\t}\n\n\tif err := pool.Ping(ctx); err != nil {\n\t\tpool.Close()\n\t\treturn nil, fmt.Errorf(\"unable to ping database: %w\", err)\n\t}\n\n\treturn pool, nil\n}\n\n// NewMysqlStoreFromSqlStore restores a store from SqlStore and stores MySQL DB.\nfunc NewMysqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) {\n\tstore, err := NewMysqlStore(ctx, dsn, metrics, false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = store.SaveInstallationID(ctx, sqliteStore.GetInstallationID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, account := range sqliteStore.GetAllAccounts(ctx) {\n\t\terr := store.SaveAccount(ctx, account)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn store, nil\n}\n\nfunc (s *SqlStore) GetSetupKeyBySecret(ctx context.Context, lockStrength LockingStrength, key string) (*types.SetupKey, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar setupKey types.SetupKey\n\tresult := tx.\n\t\tTake(&setupKey, GetKeyQueryCondition(s), key)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.PreconditionFailed, \"setup key not found\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get setup key by secret from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get setup key by secret from store\")\n\t}\n\treturn &setupKey, nil\n}\n\nfunc (s *SqlStore) IncrementSetupKeyUsage(ctx context.Context, setupKeyID string) error {\n\tresult := s.db.Model(&types.SetupKey{}).\n\t\tWhere(idQueryCondition, setupKeyID).\n\t\tUpdates(map[string]interface{}{\n\t\t\t\"used_times\": gorm.Expr(\"used_times + 1\"),\n\t\t\t\"last_used\": time.Now(),\n\t\t})\n\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"issue incrementing setup key usage count: %s\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewSetupKeyNotFoundError(setupKeyID)\n\t}\n\n\treturn nil\n}\n\n// AddPeerToAllGroup adds a peer to the 'All' group. Method always needs to run in a transaction\nfunc (s *SqlStore) AddPeerToAllGroup(ctx context.Context, accountID string, peerID string) error {\n\tvar groupID string\n\t_ = s.db.Model(types.Group{}).\n\t\tSelect(\"id\").\n\t\tWhere(\"account_id = ? AND name = ?\", accountID, \"All\").\n\t\tLimit(1).\n\t\tScan(&groupID)\n\n\tif groupID == \"\" {\n\t\treturn status.Errorf(status.NotFound, \"group 'All' not found for account %s\", accountID)\n\t}\n\n\terr := s.db.Clauses(clause.OnConflict{\n\t\tColumns: []clause.Column{{Name: \"group_id\"}, {Name: \"peer_id\"}},\n\t\tDoNothing: true,\n\t}).Create(&types.GroupPeer{\n\t\tAccountID: accountID,\n\t\tGroupID: groupID,\n\t\tPeerID: peerID,\n\t}).Error\n\n\tif err != nil {\n\t\treturn status.Errorf(status.Internal, \"error adding peer to group 'All': %v\", err)\n\t}\n\n\treturn nil\n}\n\n// AddPeerToGroup adds a peer to a group\nfunc (s *SqlStore) AddPeerToGroup(ctx context.Context, accountID, peerID, groupID string) error {\n\tpeer := &types.GroupPeer{\n\t\tAccountID: accountID,\n\t\tGroupID: groupID,\n\t\tPeerID: peerID,\n\t}\n\n\terr := s.db.Clauses(clause.OnConflict{\n\t\tColumns: []clause.Column{{Name: \"group_id\"}, {Name: \"peer_id\"}},\n\t\tDoNothing: true,\n\t}).Create(peer).Error\n\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to add peer %s to group %s for account %s: %v\", peerID, groupID, accountID, err)\n\t\treturn status.Errorf(status.Internal, \"failed to add peer to group\")\n\t}\n\n\treturn nil\n}\n\n// RemovePeerFromGroup removes a peer from a group\nfunc (s *SqlStore) RemovePeerFromGroup(ctx context.Context, peerID string, groupID string) error {\n\terr := s.db.\n\t\tDelete(&types.GroupPeer{}, \"group_id = ? AND peer_id = ?\", groupID, peerID).Error\n\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to remove peer %s from group %s: %v\", peerID, groupID, err)\n\t\treturn status.Errorf(status.Internal, \"failed to remove peer from group\")\n\t}\n\n\treturn nil\n}\n\n// RemovePeerFromAllGroups removes a peer from all groups\nfunc (s *SqlStore) RemovePeerFromAllGroups(ctx context.Context, peerID string) error {\n\terr := s.db.\n\t\tDelete(&types.GroupPeer{}, \"peer_id = ?\", peerID).Error\n\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to remove peer %s from all groups: %v\", peerID, err)\n\t\treturn status.Errorf(status.Internal, \"failed to remove peer from all groups\")\n\t}\n\n\treturn nil\n}\n\n// AddResourceToGroup adds a resource to a group. Method always needs to run n a transaction\nfunc (s *SqlStore) AddResourceToGroup(ctx context.Context, accountId string, groupID string, resource *types.Resource) error {\n\tvar group types.Group\n\tresult := s.db.Where(accountAndIDQueryCondition, accountId, groupID).Take(&group)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn status.NewGroupNotFoundError(groupID)\n\t\t}\n\n\t\treturn status.Errorf(status.Internal, \"issue finding group: %s\", result.Error)\n\t}\n\n\tfor _, res := range group.Resources {\n\t\tif res.ID == resource.ID {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tgroup.Resources = append(group.Resources, *resource)\n\n\tif err := s.db.Save(&group).Error; err != nil {\n\t\treturn status.Errorf(status.Internal, \"issue updating group: %s\", err)\n\t}\n\n\treturn nil\n}\n\n// RemoveResourceFromGroup removes a resource from a group. Method always needs to run in a transaction\nfunc (s *SqlStore) RemoveResourceFromGroup(ctx context.Context, accountId string, groupID string, resourceID string) error {\n\tvar group types.Group\n\tresult := s.db.Where(accountAndIDQueryCondition, accountId, groupID).Take(&group)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn status.NewGroupNotFoundError(groupID)\n\t\t}\n\n\t\treturn status.Errorf(status.Internal, \"issue finding group: %s\", result.Error)\n\t}\n\n\tfor i, res := range group.Resources {\n\t\tif res.ID == resourceID {\n\t\t\tgroup.Resources = append(group.Resources[:i], group.Resources[i+1:]...)\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif err := s.db.Save(&group).Error; err != nil {\n\t\treturn status.Errorf(status.Internal, \"issue updating group: %s\", err)\n\t}\n\n\treturn nil\n}\n\n// GetPeerGroups retrieves all groups assigned to a specific peer in a given account.\nfunc (s *SqlStore) GetPeerGroups(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string) ([]*types.Group, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar groups []*types.Group\n\tquery := tx.\n\t\tJoins(\"JOIN group_peers ON group_peers.group_id = groups.id\").\n\t\tWhere(\"group_peers.peer_id = ?\", peerId).\n\t\tPreload(clause.Associations).\n\t\tFind(&groups)\n\n\tif query.Error != nil {\n\t\treturn nil, query.Error\n\t}\n\n\tfor _, group := range groups {\n\t\tgroup.LoadGroupPeers()\n\t}\n\n\treturn groups, nil\n}\n\n// GetPeerGroupIDs retrieves all group IDs assigned to a specific peer in a given account.\nfunc (s *SqlStore) GetPeerGroupIDs(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string) ([]string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar groupIDs []string\n\tquery := tx.\n\t\tModel(&types.GroupPeer{}).\n\t\tWhere(\"account_id = ? AND peer_id = ?\", accountId, peerId).\n\t\tPluck(\"group_id\", &groupIDs)\n\n\tif query.Error != nil {\n\t\tif errors.Is(query.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"no groups found for peer %s in account %s\", peerId, accountId)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get group IDs for peer %s in account %s: %v\", peerId, accountId, query.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get group IDs for peer from store\")\n\t}\n\n\treturn groupIDs, nil\n}\n\n// GetAccountPeers retrieves peers for an account.\nfunc (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) {\n\tvar peers []*nbpeer.Peer\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\tquery := tx.Where(accountIDCondition, accountID)\n\n\tif nameFilter != \"\" {\n\t\tquery = query.Where(\"name LIKE ?\", \"%\"+nameFilter+\"%\")\n\t}\n\tif ipFilter != \"\" {\n\t\tquery = query.Where(\"ip LIKE ?\", \"%\"+ipFilter+\"%\")\n\t}\n\n\tif err := query.Find(&peers).Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers from store\")\n\t}\n\n\treturn peers, nil\n}\n\n// GetUserPeers retrieves peers for a user.\nfunc (s *SqlStore) GetUserPeers(ctx context.Context, lockStrength LockingStrength, accountID, userID string) ([]*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peers []*nbpeer.Peer\n\n\t// Exclude peers added via setup keys, as they are not user-specific and have an empty user_id.\n\tif userID == \"\" {\n\t\treturn peers, nil\n\t}\n\n\tresult := tx.\n\t\tFind(&peers, \"account_id = ? AND user_id = ?\", accountID, userID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers from store\")\n\t}\n\n\treturn peers, nil\n}\n\nfunc (s *SqlStore) AddPeerToAccount(ctx context.Context, peer *nbpeer.Peer) error {\n\tif err := s.db.Create(peer).Error; err != nil {\n\t\treturn status.Errorf(status.Internal, \"issue adding peer to account: %s\", err)\n\t}\n\n\treturn nil\n}\n\n// GetPeerByID retrieves a peer by its ID and account ID.\nfunc (s *SqlStore) GetPeerByID(ctx context.Context, lockStrength LockingStrength, accountID, peerID string) (*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peer *nbpeer.Peer\n\tresult := tx.\n\t\tTake(&peer, accountAndIDQueryCondition, accountID, peerID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPeerNotFoundError(peerID)\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peer from store\")\n\t}\n\n\treturn peer, nil\n}\n\n// GetPeersByIDs retrieves peers by their IDs and account ID.\nfunc (s *SqlStore) GetPeersByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, peerIDs []string) (map[string]*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peers []*nbpeer.Peer\n\tresult := tx.Find(&peers, accountAndIDsQueryCondition, accountID, peerIDs)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers by ID's from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers by ID's from the store\")\n\t}\n\n\tpeersMap := make(map[string]*nbpeer.Peer)\n\tfor _, peer := range peers {\n\t\tpeersMap[peer.ID] = peer\n\t}\n\n\treturn peersMap, nil\n}\n\n// GetAccountPeersWithExpiration retrieves a list of peers that have login expiration enabled and added by a user.\nfunc (s *SqlStore) GetAccountPeersWithExpiration(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peers []*nbpeer.Peer\n\tresult := tx.\n\t\tWhere(\"login_expiration_enabled = ? AND user_id IS NOT NULL AND user_id != ''\", true).\n\t\tFind(&peers, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers with expiration from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers with expiration from store\")\n\t}\n\n\treturn peers, nil\n}\n\n// GetAccountPeersWithInactivity retrieves a list of peers that have login expiration enabled and added by a user.\nfunc (s *SqlStore) GetAccountPeersWithInactivity(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peers []*nbpeer.Peer\n\tresult := tx.\n\t\tWhere(\"inactivity_expiration_enabled = ? AND user_id IS NOT NULL AND user_id != ''\", true).\n\t\tFind(&peers, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers with inactivity from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers with inactivity from store\")\n\t}\n\n\treturn peers, nil\n}\n\n// GetAllEphemeralPeers retrieves all peers with Ephemeral set to true across all accounts, optimized for batch processing.\nfunc (s *SqlStore) GetAllEphemeralPeers(ctx context.Context, lockStrength LockingStrength) ([]*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar allEphemeralPeers, batchPeers []*nbpeer.Peer\n\tresult := tx.\n\t\tWhere(\"ephemeral = ?\", true).\n\t\tFindInBatches(&batchPeers, 1000, func(tx *gorm.DB, batch int) error {\n\t\t\tallEphemeralPeers = append(allEphemeralPeers, batchPeers...)\n\t\t\treturn nil\n\t\t})\n\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to retrieve ephemeral peers: %s\", result.Error)\n\t\treturn nil, fmt.Errorf(\"failed to retrieve ephemeral peers\")\n\t}\n\n\treturn allEphemeralPeers, nil\n}\n\n// DeletePeer removes a peer from the store.\nfunc (s *SqlStore) DeletePeer(ctx context.Context, accountID string, peerID string) error {\n\tresult := s.db.Delete(&nbpeer.Peer{}, accountAndIDQueryCondition, accountID, peerID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete peer from the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to delete peer from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewPeerNotFoundError(peerID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) IncrementNetworkSerial(ctx context.Context, accountId string) error {\n\tresult := s.db.Model(&types.Account{}).Where(idQueryCondition, accountId).Update(\"network_serial\", gorm.Expr(\"network_serial + 1\"))\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to increment network serial count in store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to increment network serial count in store\")\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) ExecuteInTransaction(ctx context.Context, operation func(store Store) error) error {\n\ttimeoutCtx, cancel := context.WithTimeout(context.Background(), s.transactionTimeout)\n\tdefer cancel()\n\n\tstartTime := time.Now()\n\ttx := s.db.WithContext(timeoutCtx).Begin()\n\tif tx.Error != nil {\n\t\treturn tx.Error\n\t}\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\ttx.Rollback()\n\t\t\tpanic(r)\n\t\t}\n\t}()\n\n\tif s.storeEngine == types.PostgresStoreEngine {\n\t\tif err := tx.Exec(\"SET LOCAL statement_timeout = '1min'\").Error; err != nil {\n\t\t\ttx.Rollback()\n\t\t\treturn fmt.Errorf(\"failed to set statement timeout: %w\", err)\n\t\t}\n\t\tif err := tx.Exec(\"SET LOCAL lock_timeout = '1min'\").Error; err != nil {\n\t\t\ttx.Rollback()\n\t\t\treturn fmt.Errorf(\"failed to set lock timeout: %w\", err)\n\t\t}\n\t}\n\n\t// For MySQL, disable FK checks within this transaction to avoid deadlocks\n\t// This is session-scoped and doesn't require SUPER privileges\n\tif s.storeEngine == types.MysqlStoreEngine {\n\t\tif err := tx.Exec(\"SET FOREIGN_KEY_CHECKS = 0\").Error; err != nil {\n\t\t\ttx.Rollback()\n\t\t\treturn fmt.Errorf(\"failed to disable FK checks: %w\", err)\n\t\t}\n\t}\n\n\trepo := s.withTx(tx)\n\terr := operation(repo)\n\tif err != nil {\n\t\ttx.Rollback()\n\t\tif errors.Is(err, context.DeadlineExceeded) || errors.Is(timeoutCtx.Err(), context.DeadlineExceeded) {\n\t\t\tlog.WithContext(ctx).Warnf(\"transaction exceeded %s timeout after %v, stack: %s\", s.transactionTimeout, time.Since(startTime), debug.Stack())\n\t\t}\n\t\treturn err\n\t}\n\n\t// Re-enable FK checks before commit (optional, as transaction end resets it)\n\tif s.storeEngine == types.MysqlStoreEngine {\n\t\tif err := tx.Exec(\"SET FOREIGN_KEY_CHECKS = 1\").Error; err != nil {\n\t\t\ttx.Rollback()\n\t\t\treturn fmt.Errorf(\"failed to re-enable FK checks: %w\", err)\n\t\t}\n\t}\n\n\terr = tx.Commit().Error\n\tif err != nil {\n\t\tif errors.Is(err, context.DeadlineExceeded) || errors.Is(timeoutCtx.Err(), context.DeadlineExceeded) {\n\t\t\tlog.WithContext(ctx).Warnf(\"transaction commit exceeded %s timeout after %v, stack: %s\", s.transactionTimeout, time.Since(startTime), debug.Stack())\n\t\t}\n\t\treturn err\n\t}\n\n\tlog.WithContext(ctx).Tracef(\"transaction took %v\", time.Since(startTime))\n\tif s.metrics != nil {\n\t\ts.metrics.StoreMetrics().CountTransactionDuration(time.Since(startTime))\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) withTx(tx *gorm.DB) Store {\n\treturn &SqlStore{\n\t\tdb: tx,\n\t\tstoreEngine: s.storeEngine,\n\t\tfieldEncrypt: s.fieldEncrypt,\n\t}\n}\n\n// transaction wraps a GORM transaction with MySQL-specific FK checks handling\n// Use this instead of db.Transaction() directly to avoid deadlocks on MySQL/Aurora\nfunc (s *SqlStore) transaction(fn func(*gorm.DB) error) error {\n\treturn s.db.Transaction(func(tx *gorm.DB) error {\n\t\t// For MySQL, disable FK checks within this transaction to avoid deadlocks\n\t\t// This is session-scoped and doesn't require SUPER privileges\n\t\tif s.storeEngine == types.MysqlStoreEngine {\n\t\t\tif err := tx.Exec(\"SET FOREIGN_KEY_CHECKS = 0\").Error; err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to disable FK checks: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\terr := fn(tx)\n\n\t\t// Re-enable FK checks before commit (optional, as transaction end resets it)\n\t\tif s.storeEngine == types.MysqlStoreEngine && err == nil {\n\t\t\tif fkErr := tx.Exec(\"SET FOREIGN_KEY_CHECKS = 1\").Error; fkErr != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to re-enable FK checks: %w\", fkErr)\n\t\t\t}\n\t\t}\n\n\t\treturn err\n\t})\n}\n\nfunc (s *SqlStore) GetDB() *gorm.DB {\n\treturn s.db\n}\n\n// SetFieldEncrypt sets the field encryptor for encrypting sensitive user data.\nfunc (s *SqlStore) SetFieldEncrypt(enc *crypt.FieldEncrypt) {\n\ts.fieldEncrypt = enc\n}\n\nfunc (s *SqlStore) GetAccountDNSSettings(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.DNSSettings, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountDNSSettings types.AccountDNSSettings\n\tresult := tx.Model(&types.Account{}).\n\t\tTake(&accountDNSSettings, idQueryCondition, accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get dns settings from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get dns settings from store\")\n\t}\n\treturn &accountDNSSettings.DNSSettings, nil\n}\n\n// AccountExists checks whether an account exists by the given ID.\nfunc (s *SqlStore) AccountExists(ctx context.Context, lockStrength LockingStrength, id string) (bool, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountID string\n\tresult := tx.Model(&types.Account{}).\n\t\tSelect(\"id\").Take(&accountID, idQueryCondition, id)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, result.Error\n\t}\n\n\treturn accountID != \"\", nil\n}\n\n// GetAccountDomainAndCategory retrieves the Domain and DomainCategory fields for an account based on the given accountID.\nfunc (s *SqlStore) GetAccountDomainAndCategory(ctx context.Context, lockStrength LockingStrength, accountID string) (string, string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar account types.Account\n\tresult := tx.Model(&types.Account{}).Select(\"domain\", \"domain_category\").\n\t\tWhere(idQueryCondition, accountID).Take(&account)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", \"\", status.Errorf(status.NotFound, \"account not found\")\n\t\t}\n\t\treturn \"\", \"\", status.Errorf(status.Internal, \"failed to get domain category from store: %v\", result.Error)\n\t}\n\n\treturn account.Domain, account.DomainCategory, nil\n}\n\n// GetGroupByID retrieves a group by ID and account ID.\nfunc (s *SqlStore) GetGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) (*types.Group, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar group *types.Group\n\tresult := tx.Preload(clause.Associations).Take(&group, accountAndIDQueryCondition, accountID, groupID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewGroupNotFoundError(groupID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get group from store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get group from store\")\n\t}\n\n\tgroup.LoadGroupPeers()\n\n\treturn group, nil\n}\n\n// GetGroupByName retrieves a group by name and account ID.\nfunc (s *SqlStore) GetGroupByName(ctx context.Context, lockStrength LockingStrength, accountID, groupName string) (*types.Group, error) {\n\ttx := s.db\n\n\tvar group types.Group\n\n\t// TODO: This fix is accepted for now, but if we need to handle this more frequently\n\t// we may need to reconsider changing the types.\n\tquery := tx.Preload(clause.Associations)\n\n\tresult := query.\n\t\tModel(&types.Group{}).\n\t\tJoins(\"LEFT JOIN group_peers ON group_peers.group_id = groups.id\").\n\t\tWhere(\"groups.account_id = ? AND groups.name = ?\", accountID, groupName).\n\t\tGroup(\"groups.id\").\n\t\tOrder(\"COUNT(group_peers.peer_id) DESC\").\n\t\tLimit(1).\n\t\tFirst(&group)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewGroupNotFoundError(groupName)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get group by name from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get group by name from store\")\n\t}\n\n\tgroup.LoadGroupPeers()\n\n\treturn &group, nil\n}\n\n// GetGroupsByIDs retrieves groups by their IDs and account ID.\nfunc (s *SqlStore) GetGroupsByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, groupIDs []string) (map[string]*types.Group, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar groups []*types.Group\n\tresult := tx.Preload(clause.Associations).Find(&groups, accountAndIDsQueryCondition, accountID, groupIDs)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get groups by ID's from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get groups by ID's from store\")\n\t}\n\n\tgroupsMap := make(map[string]*types.Group)\n\tfor _, group := range groups {\n\t\tgroup.LoadGroupPeers()\n\t\tgroupsMap[group.ID] = group\n\t}\n\n\treturn groupsMap, nil\n}\n\n// CreateGroup creates a group in the store.\nfunc (s *SqlStore) CreateGroup(ctx context.Context, group *types.Group) error {\n\tif group == nil {\n\t\treturn status.Errorf(status.InvalidArgument, \"group is nil\")\n\t}\n\n\tif err := s.db.Omit(clause.Associations).Create(group).Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save group to store: %v\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save group to store\")\n\t}\n\n\treturn nil\n}\n\n// UpdateGroup updates a group in the store.\nfunc (s *SqlStore) UpdateGroup(ctx context.Context, group *types.Group) error {\n\tif group == nil {\n\t\treturn status.Errorf(status.InvalidArgument, \"group is nil\")\n\t}\n\n\tif err := s.db.Omit(clause.Associations).Save(group).Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save group to store: %v\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save group to store\")\n\t}\n\n\treturn nil\n}\n\n// DeleteGroup deletes a group from the database.\nfunc (s *SqlStore) DeleteGroup(ctx context.Context, accountID, groupID string) error {\n\tresult := s.db.Select(clause.Associations).\n\t\tDelete(&types.Group{}, accountAndIDQueryCondition, accountID, groupID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete group from store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete group from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewGroupNotFoundError(groupID)\n\t}\n\n\treturn nil\n}\n\n// DeleteGroups deletes groups from the database.\nfunc (s *SqlStore) DeleteGroups(ctx context.Context, accountID string, groupIDs []string) error {\n\tresult := s.db.Select(clause.Associations).\n\t\tDelete(&types.Group{}, accountAndIDsQueryCondition, accountID, groupIDs)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete groups from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete groups from store\")\n\t}\n\n\treturn nil\n}\n\n// GetAccountPolicies retrieves policies for an account.\nfunc (s *SqlStore) GetAccountPolicies(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.Policy, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar policies []*types.Policy\n\tresult := tx.\n\t\tPreload(clause.Associations).Find(&policies, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get policies from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get policies from store\")\n\t}\n\n\treturn policies, nil\n}\n\n// GetPolicyByID retrieves a policy by its ID and account ID.\nfunc (s *SqlStore) GetPolicyByID(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) (*types.Policy, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar policy *types.Policy\n\n\tresult := tx.Preload(clause.Associations).\n\t\tTake(&policy, accountAndIDQueryCondition, accountID, policyID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPolicyNotFoundError(policyID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get policy from store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get policy from store\")\n\t}\n\n\treturn policy, nil\n}\n\nfunc (s *SqlStore) CreatePolicy(ctx context.Context, policy *types.Policy) error {\n\tresult := s.db.Create(policy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create policy in store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create policy in store\")\n\t}\n\n\treturn nil\n}\n\n// SavePolicy saves a policy to the database.\nfunc (s *SqlStore) SavePolicy(ctx context.Context, policy *types.Policy) error {\n\tresult := s.db.Session(&gorm.Session{FullSaveAssociations: true}).Save(policy)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save policy to the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save policy to store\")\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) DeletePolicy(ctx context.Context, accountID, policyID string) error {\n\treturn s.transaction(func(tx *gorm.DB) error {\n\t\tif err := tx.Where(\"policy_id = ?\", policyID).Delete(&types.PolicyRule{}).Error; err != nil {\n\t\t\treturn fmt.Errorf(\"delete policy rules: %w\", err)\n\t\t}\n\n\t\tresult := tx.\n\t\t\tWhere(accountAndIDQueryCondition, accountID, policyID).\n\t\t\tDelete(&types.Policy{})\n\n\t\tif err := result.Error; err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"failed to delete policy from store: %s\", err)\n\t\t\treturn status.Errorf(status.Internal, \"failed to delete policy from store\")\n\t\t}\n\n\t\tif result.RowsAffected == 0 {\n\t\t\treturn status.NewPolicyNotFoundError(policyID)\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\nfunc (s *SqlStore) GetPolicyRulesByResourceID(ctx context.Context, lockStrength LockingStrength, accountID string, resourceID string) ([]*types.PolicyRule, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar policyRules []*types.PolicyRule\n\tresourceIDPattern := `%\"ID\":\"` + resourceID + `\"%`\n\tresult := tx.Where(\"source_resource LIKE ? OR destination_resource LIKE ?\", resourceIDPattern, resourceIDPattern).\n\t\tFind(&policyRules)\n\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get policy rules for resource id from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get policy rules for resource id from store\")\n\t}\n\n\treturn policyRules, nil\n}\n\n// GetAccountPostureChecks retrieves posture checks for an account.\nfunc (s *SqlStore) GetAccountPostureChecks(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*posture.Checks, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar postureChecks []*posture.Checks\n\tresult := tx.Find(&postureChecks, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get posture checks from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get posture checks from store\")\n\t}\n\n\treturn postureChecks, nil\n}\n\n// GetPostureChecksByID retrieves posture checks by their ID and account ID.\nfunc (s *SqlStore) GetPostureChecksByID(ctx context.Context, lockStrength LockingStrength, accountID, postureChecksID string) (*posture.Checks, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar postureCheck *posture.Checks\n\tresult := tx.\n\t\tTake(&postureCheck, accountAndIDQueryCondition, accountID, postureChecksID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPostureChecksNotFoundError(postureChecksID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get posture check from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get posture check from store\")\n\t}\n\n\treturn postureCheck, nil\n}\n\n// GetPostureChecksByIDs retrieves posture checks by their IDs and account ID.\nfunc (s *SqlStore) GetPostureChecksByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, postureChecksIDs []string) (map[string]*posture.Checks, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar postureChecks []*posture.Checks\n\tresult := tx.Find(&postureChecks, accountAndIDsQueryCondition, accountID, postureChecksIDs)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get posture checks by ID's from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get posture checks by ID's from store\")\n\t}\n\n\tpostureChecksMap := make(map[string]*posture.Checks)\n\tfor _, postureCheck := range postureChecks {\n\t\tpostureChecksMap[postureCheck.ID] = postureCheck\n\t}\n\n\treturn postureChecksMap, nil\n}\n\n// SavePostureChecks saves a posture checks to the database.\nfunc (s *SqlStore) SavePostureChecks(ctx context.Context, postureCheck *posture.Checks) error {\n\tresult := s.db.Save(postureCheck)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save posture checks to store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save posture checks to store\")\n\t}\n\n\treturn nil\n}\n\n// DeletePostureChecks deletes a posture checks from the database.\nfunc (s *SqlStore) DeletePostureChecks(ctx context.Context, accountID, postureChecksID string) error {\n\tresult := s.db.Delete(&posture.Checks{}, accountAndIDQueryCondition, accountID, postureChecksID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete posture checks from store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete posture checks from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewPostureChecksNotFoundError(postureChecksID)\n\t}\n\n\treturn nil\n}\n\n// GetAccountRoutes retrieves network routes for an account.\nfunc (s *SqlStore) GetAccountRoutes(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*route.Route, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar routes []*route.Route\n\tresult := tx.Find(&routes, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get routes from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get routes from store\")\n\t}\n\n\treturn routes, nil\n}\n\n// GetRouteByID retrieves a route by its ID and account ID.\nfunc (s *SqlStore) GetRouteByID(ctx context.Context, lockStrength LockingStrength, accountID string, routeID string) (*route.Route, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar route *route.Route\n\tresult := tx.Take(&route, accountAndIDQueryCondition, accountID, routeID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewRouteNotFoundError(routeID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get route from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get route from store\")\n\t}\n\n\treturn route, nil\n}\n\n// SaveRoute saves a route to the database.\nfunc (s *SqlStore) SaveRoute(ctx context.Context, route *route.Route) error {\n\tresult := s.db.Save(route)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save route to the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save route to store\")\n\t}\n\n\treturn nil\n}\n\n// DeleteRoute deletes a route from the database.\nfunc (s *SqlStore) DeleteRoute(ctx context.Context, accountID, routeID string) error {\n\tresult := s.db.Delete(&route.Route{}, accountAndIDQueryCondition, accountID, routeID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete route from the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to delete route from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewRouteNotFoundError(routeID)\n\t}\n\n\treturn nil\n}\n\n// GetAccountSetupKeys retrieves setup keys for an account.\nfunc (s *SqlStore) GetAccountSetupKeys(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.SetupKey, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar setupKeys []*types.SetupKey\n\tresult := tx.\n\t\tFind(&setupKeys, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get setup keys from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get setup keys from store\")\n\t}\n\n\treturn setupKeys, nil\n}\n\n// GetSetupKeyByID retrieves a setup key by its ID and account ID.\nfunc (s *SqlStore) GetSetupKeyByID(ctx context.Context, lockStrength LockingStrength, accountID, setupKeyID string) (*types.SetupKey, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar setupKey *types.SetupKey\n\tresult := tx.Take(&setupKey, accountAndIDQueryCondition, accountID, setupKeyID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewSetupKeyNotFoundError(setupKeyID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get setup key from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get setup key from store\")\n\t}\n\n\treturn setupKey, nil\n}\n\n// SaveSetupKey saves a setup key to the database.\nfunc (s *SqlStore) SaveSetupKey(ctx context.Context, setupKey *types.SetupKey) error {\n\tresult := s.db.Save(setupKey)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save setup key to store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save setup key to store\")\n\t}\n\n\treturn nil\n}\n\n// DeleteSetupKey deletes a setup key from the database.\nfunc (s *SqlStore) DeleteSetupKey(ctx context.Context, accountID, keyID string) error {\n\tresult := s.db.Delete(&types.SetupKey{}, accountAndIDQueryCondition, accountID, keyID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete setup key from store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete setup key from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewSetupKeyNotFoundError(keyID)\n\t}\n\n\treturn nil\n}\n\n// GetAccountNameServerGroups retrieves name server groups for an account.\nfunc (s *SqlStore) GetAccountNameServerGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbdns.NameServerGroup, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar nsGroups []*nbdns.NameServerGroup\n\tresult := tx.Find(&nsGroups, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get name server groups from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get name server groups from store\")\n\t}\n\n\treturn nsGroups, nil\n}\n\n// GetNameServerGroupByID retrieves a name server group by its ID and account ID.\nfunc (s *SqlStore) GetNameServerGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, nsGroupID string) (*nbdns.NameServerGroup, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar nsGroup *nbdns.NameServerGroup\n\tresult := tx.\n\t\tTake(&nsGroup, accountAndIDQueryCondition, accountID, nsGroupID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewNameServerGroupNotFoundError(nsGroupID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get name server group from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get name server group from store\")\n\t}\n\n\treturn nsGroup, nil\n}\n\n// SaveNameServerGroup saves a name server group to the database.\nfunc (s *SqlStore) SaveNameServerGroup(ctx context.Context, nameServerGroup *nbdns.NameServerGroup) error {\n\tresult := s.db.Save(nameServerGroup)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save name server group to the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save name server group to store\")\n\t}\n\treturn nil\n}\n\n// DeleteNameServerGroup deletes a name server group from the database.\nfunc (s *SqlStore) DeleteNameServerGroup(ctx context.Context, accountID, nsGroupID string) error {\n\tresult := s.db.Delete(&nbdns.NameServerGroup{}, accountAndIDQueryCondition, accountID, nsGroupID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete name server group from the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to delete name server group from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewNameServerGroupNotFoundError(nsGroupID)\n\t}\n\n\treturn nil\n}\n\n// SaveDNSSettings saves the DNS settings to the store.\nfunc (s *SqlStore) SaveDNSSettings(ctx context.Context, accountID string, settings *types.DNSSettings) error {\n\tresult := s.db.Model(&types.Account{}).\n\t\tWhere(idQueryCondition, accountID).Updates(&types.AccountDNSSettings{DNSSettings: *settings})\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save dns settings to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save dns settings to store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewAccountNotFoundError(accountID)\n\t}\n\n\treturn nil\n}\n\n// SaveAccountSettings stores the account settings in DB.\nfunc (s *SqlStore) SaveAccountSettings(ctx context.Context, accountID string, settings *types.Settings) error {\n\tresult := s.db.Model(&types.Account{}).\n\t\tSelect(\"*\").Where(idQueryCondition, accountID).Updates(&types.AccountSettings{Settings: settings})\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save account settings to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save account settings to store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewAccountNotFoundError(accountID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetAccountNetworks(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*networkTypes.Network, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar networks []*networkTypes.Network\n\tresult := tx.Find(&networks, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get networks from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get networks from store\")\n\t}\n\n\treturn networks, nil\n}\n\nfunc (s *SqlStore) GetNetworkByID(ctx context.Context, lockStrength LockingStrength, accountID, networkID string) (*networkTypes.Network, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar network *networkTypes.Network\n\tresult := tx.Take(&network, accountAndIDQueryCondition, accountID, networkID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewNetworkNotFoundError(networkID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network from store\")\n\t}\n\n\treturn network, nil\n}\n\nfunc (s *SqlStore) SaveNetwork(ctx context.Context, network *networkTypes.Network) error {\n\tresult := s.db.Save(network)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save network to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save network to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteNetwork(ctx context.Context, accountID, networkID string) error {\n\tresult := s.db.Delete(&networkTypes.Network{}, accountAndIDQueryCondition, accountID, networkID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete network from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete network from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewNetworkNotFoundError(networkID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetNetworkRoutersByNetID(ctx context.Context, lockStrength LockingStrength, accountID, netID string) ([]*routerTypes.NetworkRouter, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netRouters []*routerTypes.NetworkRouter\n\tresult := tx.\n\t\tFind(&netRouters, \"account_id = ? AND network_id = ?\", accountID, netID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network routers from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network routers from store\")\n\t}\n\n\treturn netRouters, nil\n}\n\nfunc (s *SqlStore) GetNetworkRoutersByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*routerTypes.NetworkRouter, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netRouters []*routerTypes.NetworkRouter\n\tresult := tx.\n\t\tFind(&netRouters, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network routers from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network routers from store\")\n\t}\n\n\treturn netRouters, nil\n}\n\nfunc (s *SqlStore) GetNetworkRouterByID(ctx context.Context, lockStrength LockingStrength, accountID, routerID string) (*routerTypes.NetworkRouter, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netRouter *routerTypes.NetworkRouter\n\tresult := tx.\n\t\tTake(&netRouter, accountAndIDQueryCondition, accountID, routerID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewNetworkRouterNotFoundError(routerID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network router from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network router from store\")\n\t}\n\n\treturn netRouter, nil\n}\n\nfunc (s *SqlStore) SaveNetworkRouter(ctx context.Context, router *routerTypes.NetworkRouter) error {\n\tresult := s.db.Save(router)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save network router to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save network router to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteNetworkRouter(ctx context.Context, accountID, routerID string) error {\n\tresult := s.db.Delete(&routerTypes.NetworkRouter{}, accountAndIDQueryCondition, accountID, routerID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete network router from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete network router from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewNetworkRouterNotFoundError(routerID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetNetworkResourcesByNetID(ctx context.Context, lockStrength LockingStrength, accountID, networkID string) ([]*resourceTypes.NetworkResource, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netResources []*resourceTypes.NetworkResource\n\tresult := tx.\n\t\tFind(&netResources, \"account_id = ? AND network_id = ?\", accountID, networkID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network resources from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network resources from store\")\n\t}\n\n\treturn netResources, nil\n}\n\nfunc (s *SqlStore) GetNetworkResourcesByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*resourceTypes.NetworkResource, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netResources []*resourceTypes.NetworkResource\n\tresult := tx.\n\t\tFind(&netResources, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network resources from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network resources from store\")\n\t}\n\n\treturn netResources, nil\n}\n\nfunc (s *SqlStore) GetNetworkResourceByID(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) (*resourceTypes.NetworkResource, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netResources *resourceTypes.NetworkResource\n\tresult := tx.\n\t\tTake(&netResources, accountAndIDQueryCondition, accountID, resourceID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewNetworkResourceNotFoundError(resourceID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network resource from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network resource from store\")\n\t}\n\n\treturn netResources, nil\n}\n\nfunc (s *SqlStore) GetNetworkResourceByName(ctx context.Context, lockStrength LockingStrength, accountID, resourceName string) (*resourceTypes.NetworkResource, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netResources *resourceTypes.NetworkResource\n\tresult := tx.\n\t\tTake(&netResources, \"account_id = ? AND name = ?\", accountID, resourceName)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewNetworkResourceNotFoundError(resourceName)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network resource from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network resource from store\")\n\t}\n\n\treturn netResources, nil\n}\n\nfunc (s *SqlStore) SaveNetworkResource(ctx context.Context, resource *resourceTypes.NetworkResource) error {\n\tresult := s.db.Save(resource)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save network resource to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save network resource to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteNetworkResource(ctx context.Context, accountID, resourceID string) error {\n\tresult := s.db.Delete(&resourceTypes.NetworkResource{}, accountAndIDQueryCondition, accountID, resourceID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete network resource from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete network resource from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewNetworkResourceNotFoundError(resourceID)\n\t}\n\n\treturn nil\n}\n\n// GetPATByHashedToken returns a PersonalAccessToken by its hashed token.\nfunc (s *SqlStore) GetPATByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken string) (*types.PersonalAccessToken, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar pat types.PersonalAccessToken\n\tresult := tx.Take(&pat, \"hashed_token = ?\", hashedToken)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPATNotFoundError(hashedToken)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get pat by hash from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get pat by hash from store\")\n\t}\n\n\treturn &pat, nil\n}\n\n// GetPATByID retrieves a personal access token by its ID and user ID.\nfunc (s *SqlStore) GetPATByID(ctx context.Context, lockStrength LockingStrength, userID string, patID string) (*types.PersonalAccessToken, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar pat types.PersonalAccessToken\n\tresult := tx.\n\t\tTake(&pat, \"id = ? AND user_id = ?\", patID, userID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPATNotFoundError(patID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get pat from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get pat from store\")\n\t}\n\n\treturn &pat, nil\n}\n\n// GetUserPATs retrieves personal access tokens for a user.\nfunc (s *SqlStore) GetUserPATs(ctx context.Context, lockStrength LockingStrength, userID string) ([]*types.PersonalAccessToken, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar pats []*types.PersonalAccessToken\n\tresult := tx.Find(&pats, \"user_id = ?\", userID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get user pat's from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get user pat's from store\")\n\t}\n\n\treturn pats, nil\n}\n\n// MarkPATUsed marks a personal access token as used.\nfunc (s *SqlStore) MarkPATUsed(ctx context.Context, patID string) error {\n\tpatCopy := types.PersonalAccessToken{\n\t\tLastUsed: util.ToPtr(time.Now().UTC()),\n\t}\n\n\tfieldsToUpdate := []string{\"last_used\"}\n\tresult := s.db.Select(fieldsToUpdate).\n\t\tWhere(idQueryCondition, patID).Updates(&patCopy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to mark pat as used: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to mark pat as used\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewPATNotFoundError(patID)\n\t}\n\n\treturn nil\n}\n\n// SavePAT saves a personal access token to the database.\nfunc (s *SqlStore) SavePAT(ctx context.Context, pat *types.PersonalAccessToken) error {\n\tresult := s.db.Save(pat)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save pat to the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save pat to store\")\n\t}\n\n\treturn nil\n}\n\n// DeletePAT deletes a personal access token from the database.\nfunc (s *SqlStore) DeletePAT(ctx context.Context, userID, patID string) error {\n\tresult := s.db.Delete(&types.PersonalAccessToken{}, \"user_id = ? AND id = ?\", userID, patID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete pat from the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to delete pat from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewPATNotFoundError(patID)\n\t}\n\n\treturn nil\n}\n\n// GetProxyAccessTokenByHashedToken retrieves a proxy access token by its hashed value.\nfunc (s *SqlStore) GetProxyAccessTokenByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken types.HashedProxyToken) (*types.ProxyAccessToken, error) {\n\ttx := s.db.WithContext(ctx)\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar token types.ProxyAccessToken\n\tresult := tx.Take(&token, \"hashed_token = ?\", hashedToken)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"proxy access token not found\")\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"get proxy access token: %v\", result.Error)\n\t}\n\n\treturn &token, nil\n}\n\n// GetAllProxyAccessTokens retrieves all proxy access tokens.\nfunc (s *SqlStore) GetAllProxyAccessTokens(ctx context.Context, lockStrength LockingStrength) ([]*types.ProxyAccessToken, error) {\n\ttx := s.db.WithContext(ctx)\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar tokens []*types.ProxyAccessToken\n\tresult := tx.Find(&tokens)\n\tif result.Error != nil {\n\t\treturn nil, status.Errorf(status.Internal, \"get proxy access tokens: %v\", result.Error)\n\t}\n\n\treturn tokens, nil\n}\n\n// SaveProxyAccessToken saves a proxy access token to the database.\nfunc (s *SqlStore) SaveProxyAccessToken(ctx context.Context, token *types.ProxyAccessToken) error {\n\tif result := s.db.WithContext(ctx).Create(token); result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"save proxy access token: %v\", result.Error)\n\t}\n\treturn nil\n}\n\n// RevokeProxyAccessToken revokes a proxy access token by its ID.\nfunc (s *SqlStore) RevokeProxyAccessToken(ctx context.Context, tokenID string) error {\n\tresult := s.db.WithContext(ctx).Model(&types.ProxyAccessToken{}).Where(idQueryCondition, tokenID).Update(\"revoked\", true)\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"revoke proxy access token: %v\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"proxy access token not found\")\n\t}\n\n\treturn nil\n}\n\n// MarkProxyAccessTokenUsed updates the last used timestamp for a proxy access token.\nfunc (s *SqlStore) MarkProxyAccessTokenUsed(ctx context.Context, tokenID string) error {\n\tresult := s.db.WithContext(ctx).Model(&types.ProxyAccessToken{}).\n\t\tWhere(idQueryCondition, tokenID).\n\t\tUpdate(\"last_used\", time.Now().UTC())\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"mark proxy access token as used: %v\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"proxy access token not found\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetPeerByIP(ctx context.Context, lockStrength LockingStrength, accountID string, ip net.IP) (*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tjsonValue := fmt.Sprintf(`\"%s\"`, ip.String())\n\n\tvar peer nbpeer.Peer\n\tresult := tx.\n\t\tTake(&peer, \"account_id = ? AND ip = ?\", accountID, jsonValue)\n\tif result.Error != nil {\n\t\t// no logging here\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peer from store\")\n\t}\n\n\treturn &peer, nil\n}\n\nfunc (s *SqlStore) GetPeerIdByLabel(ctx context.Context, lockStrength LockingStrength, accountID string, hostname string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peerID string\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tSelect(\"id\").\n\t\t// Where(\" = ?\", hostname).\n\t\tWhere(\"account_id = ? AND dns_label = ?\", accountID, hostname).\n\t\tLimit(1).\n\t\tScan(&peerID)\n\n\tif peerID == \"\" {\n\t\treturn \"\", gorm.ErrRecordNotFound\n\t}\n\n\treturn peerID, result.Error\n}\n\nfunc (s *SqlStore) CountAccountsByPrivateDomain(ctx context.Context, domain string) (int64, error) {\n\tvar count int64\n\tresult := s.db.Model(&types.Account{}).\n\t\tWhere(\"domain = ? AND domain_category = ?\",\n\t\t\tstrings.ToLower(domain), types.PrivateCategory,\n\t\t).Count(&count)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to count accounts by private domain %s: %s\", domain, result.Error)\n\t\treturn 0, status.Errorf(status.Internal, \"failed to count accounts by private domain\")\n\t}\n\n\treturn count, nil\n}\n\nfunc (s *SqlStore) GetAccountGroupPeers(ctx context.Context, lockStrength LockingStrength, accountID string) (map[string]map[string]struct{}, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peers []types.GroupPeer\n\tresult := tx.Find(&peers, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get account group peers from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get account group peers from store\")\n\t}\n\n\tgroupPeers := make(map[string]map[string]struct{})\n\tfor _, peer := range peers {\n\t\tif _, exists := groupPeers[peer.GroupID]; !exists {\n\t\t\tgroupPeers[peer.GroupID] = make(map[string]struct{})\n\t\t}\n\t\tgroupPeers[peer.GroupID][peer.PeerID] = struct{}{}\n\t}\n\n\treturn groupPeers, nil\n}\n\nfunc (s *SqlStore) IsPrimaryAccount(ctx context.Context, accountID string) (bool, string, error) {\n\tvar info types.PrimaryAccountInfo\n\tresult := s.db.Model(&types.Account{}).\n\t\tSelect(\"is_domain_primary_account, domain\").\n\t\tWhere(idQueryCondition, accountID).\n\t\tTake(&info)\n\n\tif result.Error != nil {\n\t\treturn false, \"\", status.Errorf(status.Internal, \"failed to get account info: %v\", result.Error)\n\t}\n\n\treturn info.IsDomainPrimaryAccount, info.Domain, nil\n}\n\nfunc (s *SqlStore) MarkAccountPrimary(ctx context.Context, accountID string) error {\n\tresult := s.db.Model(&types.Account{}).\n\t\tWhere(idQueryCondition, accountID).\n\t\tUpdate(\"is_domain_primary_account\", true)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to mark account as primary: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to mark account as primary\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewAccountNotFoundError(accountID)\n\t}\n\n\treturn nil\n}\n\ntype accountNetworkPatch struct {\n\tNetwork *types.Network `gorm:\"embedded;embeddedPrefix:network_\"`\n}\n\nfunc (s *SqlStore) UpdateAccountNetwork(ctx context.Context, accountID string, ipNet net.IPNet) error {\n\tpatch := accountNetworkPatch{\n\t\tNetwork: &types.Network{Net: ipNet},\n\t}\n\n\tresult := s.db.\n\t\tModel(&types.Account{}).\n\t\tWhere(idQueryCondition, accountID).\n\t\tUpdates(&patch)\n\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update account network: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to update account network\")\n\t}\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewAccountNotFoundError(accountID)\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) GetPeersByGroupIDs(ctx context.Context, accountID string, groupIDs []string) ([]*nbpeer.Peer, error) {\n\tif len(groupIDs) == 0 {\n\t\treturn []*nbpeer.Peer{}, nil\n\t}\n\n\tvar peers []*nbpeer.Peer\n\tpeerIDsSubquery := s.db.Model(&types.GroupPeer{}).\n\t\tSelect(\"DISTINCT peer_id\").\n\t\tWhere(\"account_id = ? AND group_id IN ?\", accountID, groupIDs)\n\n\tresult := s.db.Where(\"id IN (?)\", peerIDsSubquery).Find(&peers)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers by group IDs: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers by group IDs\")\n\t}\n\n\treturn peers, nil\n}\n\nfunc (s *SqlStore) GetUserIDByPeerKey(ctx context.Context, lockStrength LockingStrength, peerKey string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar userID string\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tSelect(\"user_id\").\n\t\tTake(&userID, GetKeyQueryCondition(s), peerKey)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"peer not found: index lookup failed\")\n\t\t}\n\t\treturn \"\", status.Errorf(status.Internal, \"failed to get user ID by peer key\")\n\t}\n\n\treturn userID, nil\n}\n\nfunc (s *SqlStore) CreateZone(ctx context.Context, zone *zones.Zone) error {\n\tresult := s.db.Create(zone)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create zone to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create zone to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) UpdateZone(ctx context.Context, zone *zones.Zone) error {\n\tresult := s.db.Select(\"*\").Save(zone)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update zone to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to update zone to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteZone(ctx context.Context, accountID, zoneID string) error {\n\tresult := s.db.Delete(&zones.Zone{}, accountAndIDQueryCondition, accountID, zoneID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete zone from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete zone from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewZoneNotFoundError(zoneID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetZoneByID(ctx context.Context, lockStrength LockingStrength, accountID, zoneID string) (*zones.Zone, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar zone *zones.Zone\n\tresult := tx.Preload(\"Records\").Take(&zone, accountAndIDQueryCondition, accountID, zoneID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewZoneNotFoundError(zoneID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get zone from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get zone from store\")\n\t}\n\n\treturn zone, nil\n}\n\nfunc (s *SqlStore) GetZoneByDomain(ctx context.Context, accountID, domain string) (*zones.Zone, error) {\n\tvar zone *zones.Zone\n\tresult := s.db.Where(\"account_id = ? AND domain = ?\", accountID, domain).First(&zone)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewZoneNotFoundError(domain)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get zone by domain from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get zone by domain from store\")\n\t}\n\n\treturn zone, nil\n}\n\nfunc (s *SqlStore) GetAccountZones(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*zones.Zone, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar zones []*zones.Zone\n\tresult := tx.Preload(\"Records\").Find(&zones, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get zones from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get zones from store\")\n\t}\n\n\treturn zones, nil\n}\n\nfunc (s *SqlStore) CreateDNSRecord(ctx context.Context, record *records.Record) error {\n\tresult := s.db.Create(record)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create dns record to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create dns record to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) UpdateDNSRecord(ctx context.Context, record *records.Record) error {\n\tresult := s.db.Select(\"*\").Save(record)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update dns record to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to update dns record to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteDNSRecord(ctx context.Context, accountID, zoneID, recordID string) error {\n\tresult := s.db.Delete(&records.Record{}, \"account_id = ? AND zone_id = ? AND id = ?\", accountID, zoneID, recordID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete dns record from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete dns record from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewDNSRecordNotFoundError(recordID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetDNSRecordByID(ctx context.Context, lockStrength LockingStrength, accountID, zoneID, recordID string) (*records.Record, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar record *records.Record\n\tresult := tx.Where(\"account_id = ? AND zone_id = ? AND id = ?\", accountID, zoneID, recordID).Take(&record)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewDNSRecordNotFoundError(recordID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get dns record from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get dns record from store\")\n\t}\n\n\treturn record, nil\n}\n\nfunc (s *SqlStore) GetZoneDNSRecords(ctx context.Context, lockStrength LockingStrength, accountID, zoneID string) ([]*records.Record, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar recordsList []*records.Record\n\tresult := tx.Where(\"account_id = ? AND zone_id = ?\", accountID, zoneID).Find(&recordsList)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get zone dns records from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get zone dns records from store\")\n\t}\n\n\treturn recordsList, nil\n}\n\nfunc (s *SqlStore) GetZoneDNSRecordsByName(ctx context.Context, lockStrength LockingStrength, accountID, zoneID, name string) ([]*records.Record, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar recordsList []*records.Record\n\tresult := tx.Where(\"account_id = ? AND zone_id = ? AND name = ?\", accountID, zoneID, name).Find(&recordsList)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get zone dns records by name from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get zone dns records by name from store\")\n\t}\n\n\treturn recordsList, nil\n}\n\nfunc (s *SqlStore) DeleteZoneDNSRecords(ctx context.Context, accountID, zoneID string) error {\n\tresult := s.db.Delete(&records.Record{}, \"account_id = ? AND zone_id = ?\", accountID, zoneID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete zone dns records from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete zone dns records from store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetPeerIDByKey(ctx context.Context, lockStrength LockingStrength, key string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peerID string\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tSelect(\"id\").\n\t\tWhere(GetKeyQueryCondition(s), key).\n\t\tLimit(1).\n\t\tScan(&peerID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peer ID by key: %s\", result.Error)\n\t\treturn \"\", status.Errorf(status.Internal, \"failed to get peer ID by key\")\n\t}\n\n\treturn peerID, nil\n}\n\nfunc (s *SqlStore) CreateService(ctx context.Context, service *reverseproxy.Service) error {\n\tserviceCopy := service.Copy()\n\tif err := serviceCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn fmt.Errorf(\"encrypt service data: %w\", err)\n\t}\n\tresult := s.db.Create(serviceCopy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create service to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create service to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) UpdateService(ctx context.Context, service *reverseproxy.Service) error {\n\tserviceCopy := service.Copy()\n\tif err := serviceCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn fmt.Errorf(\"encrypt service data: %w\", err)\n\t}\n\n\t// Use a transaction to ensure atomic updates of the service and its targets\n\terr := s.db.Transaction(func(tx *gorm.DB) error {\n\t\t// Delete existing targets\n\t\tif err := tx.Where(\"service_id = ?\", serviceCopy.ID).Delete(&reverseproxy.Target{}).Error; err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Update the service and create new targets\n\t\tif err := tx.Session(&gorm.Session{FullSaveAssociations: true}).Save(serviceCopy).Error; err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update service to store: %v\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to update service to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteService(ctx context.Context, accountID, serviceID string) error {\n\tresult := s.db.Delete(&reverseproxy.Service{}, accountAndIDQueryCondition, accountID, serviceID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete service from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete service from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"service %s not found\", serviceID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteTarget(ctx context.Context, accountID string, serviceID string, targetID uint) error {\n\tresult := s.db.Delete(&reverseproxy.Target{}, \"account_id = ? AND service_id = ? AND id = ?\", accountID, serviceID, targetID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete target from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete target from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"target not found for service %s\", serviceID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteServiceTargets(ctx context.Context, accountID string, serviceID string) error {\n\tresult := s.db.Delete(&reverseproxy.Target{}, \"account_id = ? AND service_id = ?\", accountID, serviceID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete targets from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete targets from store\")\n\t}\n\n\treturn nil\n}\n\n// GetTargetsByServiceID retrieves all targets for a given service\nfunc (s *SqlStore) GetTargetsByServiceID(ctx context.Context, lockStrength LockingStrength, accountID string, serviceID string) ([]*reverseproxy.Target, error) {\n\tvar targets []*reverseproxy.Target\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\tresult := tx.Where(\"account_id = ? AND service_id = ?\", accountID, serviceID).Find(&targets)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get targets from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get targets from store\")\n\t}\n\n\treturn targets, nil\n}\n\nfunc (s *SqlStore) GetServiceByID(ctx context.Context, lockStrength LockingStrength, accountID, serviceID string) (*reverseproxy.Service, error) {\n\ttx := s.db.Preload(\"Targets\")\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar service *reverseproxy.Service\n\tresult := tx.Take(&service, accountAndIDQueryCondition, accountID, serviceID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"service %s not found\", serviceID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get service from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get service from store\")\n\t}\n\n\tif err := service.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt service data: %w\", err)\n\t}\n\n\treturn service, nil\n}\n\nfunc (s *SqlStore) GetServicesByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*reverseproxy.Service, error) {\n\ttx := s.db.Preload(\"Targets\")\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar serviceList []*reverseproxy.Service\n\tresult := tx.Find(&serviceList, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get services from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get services from store\")\n\t}\n\n\tfor _, service := range serviceList {\n\t\tif err := service.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt service data: %w\", err)\n\t\t}\n\t}\n\n\treturn serviceList, nil\n}\n\nfunc (s *SqlStore) GetServiceByDomain(ctx context.Context, accountID, domain string) (*reverseproxy.Service, error) {\n\tvar service *reverseproxy.Service\n\tresult := s.db.Preload(\"Targets\").Where(\"account_id = ? AND domain = ?\", accountID, domain).First(&service)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"service with domain %s not found\", domain)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get service by domain from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get service by domain from store\")\n\t}\n\n\tif err := service.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt service data: %w\", err)\n\t}\n\n\treturn service, nil\n}\n\nfunc (s *SqlStore) GetServices(ctx context.Context, lockStrength LockingStrength) ([]*reverseproxy.Service, error) {\n\ttx := s.db.Preload(\"Targets\")\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar serviceList []*reverseproxy.Service\n\tresult := tx.Find(&serviceList)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get services from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get services from store\")\n\t}\n\n\tfor _, service := range serviceList {\n\t\tif err := service.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt service data: %w\", err)\n\t\t}\n\t}\n\n\treturn serviceList, nil\n}\n\nfunc (s *SqlStore) GetAccountServices(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*reverseproxy.Service, error) {\n\ttx := s.db.Preload(\"Targets\")\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar serviceList []*reverseproxy.Service\n\tresult := tx.Find(&serviceList, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get services from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get services from store\")\n\t}\n\n\tfor _, service := range serviceList {\n\t\tif err := service.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt service data: %w\", err)\n\t\t}\n\t}\n\n\treturn serviceList, nil\n}\n\nfunc (s *SqlStore) GetCustomDomain(ctx context.Context, accountID string, domainID string) (*domain.Domain, error) {\n\ttx := s.db\n\n\tcustomDomain := &domain.Domain{}\n\tresult := tx.Take(&customDomain, accountAndIDQueryCondition, accountID, domainID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"custom domain %s not found\", domainID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get custom domain from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get custom domain from store\")\n\t}\n\n\treturn customDomain, nil\n}\n\nfunc (s *SqlStore) ListFreeDomains(ctx context.Context, accountID string) ([]string, error) {\n\treturn nil, nil\n}\n\nfunc (s *SqlStore) ListCustomDomains(ctx context.Context, accountID string) ([]*domain.Domain, error) {\n\ttx := s.db\n\n\tvar domains []*domain.Domain\n\tresult := tx.Find(&domains, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get reverse proxy custom domains from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get reverse proxy custom domains from store\")\n\t}\n\n\treturn domains, nil\n}\n\nfunc (s *SqlStore) CreateCustomDomain(ctx context.Context, accountID string, domainName string, targetCluster string, validated bool) (*domain.Domain, error) {\n\tnewDomain := &domain.Domain{\n\t\tID: xid.New().String(), // Generate our own ID because gorm doesn't always configure the database to handle this for us.\n\t\tDomain: domainName,\n\t\tAccountID: accountID,\n\t\tTargetCluster: targetCluster,\n\t\tType: domain.TypeCustom,\n\t\tValidated: validated,\n\t}\n\tresult := s.db.Create(newDomain)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create reverse proxy custom domain to store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to create reverse proxy custom domain to store\")\n\t}\n\n\treturn newDomain, nil\n}\n\nfunc (s *SqlStore) UpdateCustomDomain(ctx context.Context, accountID string, d *domain.Domain) (*domain.Domain, error) {\n\td.AccountID = accountID\n\tresult := s.db.Select(\"*\").Save(d)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update reverse proxy custom domain to store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to update reverse proxy custom domain to store\")\n\t}\n\n\treturn d, nil\n}\n\nfunc (s *SqlStore) DeleteCustomDomain(ctx context.Context, accountID string, domainID string) error {\n\tresult := s.db.Delete(domain.Domain{}, accountAndIDQueryCondition, accountID, domainID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete reverse proxy custom domain from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete reverse proxy custom domain from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"reverse proxy custom domain %s not found\", domainID)\n\t}\n\n\treturn nil\n}\n\n// CreateAccessLog creates a new access log entry in the database\nfunc (s *SqlStore) CreateAccessLog(ctx context.Context, logEntry *accesslogs.AccessLogEntry) error {\n\tresult := s.db.Create(logEntry)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).WithFields(log.Fields{\n\t\t\t\"service_id\": logEntry.ServiceID,\n\t\t\t\"method\": logEntry.Method,\n\t\t\t\"host\": logEntry.Host,\n\t\t\t\"path\": logEntry.Path,\n\t\t}).Errorf(\"failed to create access log entry in store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create access log entry in store\")\n\t}\n\treturn nil\n}\n\n// GetAccountAccessLogs retrieves access logs for a given account with pagination and filtering\nfunc (s *SqlStore) GetAccountAccessLogs(ctx context.Context, lockStrength LockingStrength, accountID string, filter accesslogs.AccessLogFilter) ([]*accesslogs.AccessLogEntry, int64, error) {\n\tvar logs []*accesslogs.AccessLogEntry\n\tvar totalCount int64\n\n\tbaseQuery := s.db.WithContext(ctx).\n\t\tModel(&accesslogs.AccessLogEntry{}).\n\t\tWhere(accountIDCondition, accountID)\n\n\tbaseQuery = s.applyAccessLogFilters(baseQuery, filter)\n\n\tif err := baseQuery.Count(&totalCount).Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to count access logs: %v\", err)\n\t\treturn nil, 0, status.Errorf(status.Internal, \"failed to count access logs\")\n\t}\n\n\tquery := s.db.WithContext(ctx).\n\t\tWhere(accountIDCondition, accountID)\n\n\tquery = s.applyAccessLogFilters(query, filter)\n\n\tsortColumns := filter.GetSortColumn()\n\tsortOrder := strings.ToUpper(filter.GetSortOrder())\n\n\tvar orderClauses []string\n\tfor _, col := range strings.Split(sortColumns, \",\") {\n\t\tcol = strings.TrimSpace(col)\n\t\tif col != \"\" {\n\t\t\torderClauses = append(orderClauses, col+\" \"+sortOrder)\n\t\t}\n\t}\n\torderClause := strings.Join(orderClauses, \", \")\n\n\tquery = query.\n\t\tOrder(orderClause).\n\t\tLimit(filter.GetLimit()).\n\t\tOffset(filter.GetOffset())\n\n\tif lockStrength != LockingStrengthNone {\n\t\tquery = query.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tresult := query.Find(&logs)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get access logs from store: %v\", result.Error)\n\t\treturn nil, 0, status.Errorf(status.Internal, \"failed to get access logs from store\")\n\t}\n\n\treturn logs, totalCount, nil\n}\n\n// DeleteOldAccessLogs deletes all access logs older than the specified time\nfunc (s *SqlStore) DeleteOldAccessLogs(ctx context.Context, olderThan time.Time) (int64, error) {\n\tresult := s.db.WithContext(ctx).\n\t\tWhere(\"timestamp < ?\", olderThan).\n\t\tDelete(&accesslogs.AccessLogEntry{})\n\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete old access logs: %v\", result.Error)\n\t\treturn 0, status.Errorf(status.Internal, \"failed to delete old access logs\")\n\t}\n\n\treturn result.RowsAffected, nil\n}\n\n// applyAccessLogFilters applies filter conditions to the query\nfunc (s *SqlStore) applyAccessLogFilters(query *gorm.DB, filter accesslogs.AccessLogFilter) *gorm.DB {\n\tif filter.Search != nil {\n\t\tsearchPattern := \"%\" + *filter.Search + \"%\"\n\t\tquery = query.Where(\n\t\t\t\"id LIKE ? OR location_connection_ip LIKE ? OR host LIKE ? OR path LIKE ? OR CONCAT(host, path) LIKE ? OR user_id IN (SELECT id FROM users WHERE email LIKE ? OR name LIKE ?)\",\n\t\t\tsearchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern,\n\t\t)\n\t}\n\n\tif filter.SourceIP != nil {\n\t\tquery = query.Where(\"location_connection_ip = ?\", *filter.SourceIP)\n\t}\n\n\tif filter.Host != nil {\n\t\tquery = query.Where(\"host = ?\", *filter.Host)\n\t}\n\n\tif filter.Path != nil {\n\t\t// Support LIKE pattern for path filtering\n\t\tquery = query.Where(\"path LIKE ?\", \"%\"+*filter.Path+\"%\")\n\t}\n\n\tif filter.UserID != nil {\n\t\tquery = query.Where(\"user_id = ?\", *filter.UserID)\n\t}\n\n\tif filter.Method != nil {\n\t\tquery = query.Where(\"method = ?\", *filter.Method)\n\t}\n\n\tif filter.Status != nil {\n\t\tswitch *filter.Status {\n\t\tcase \"success\":\n\t\t\tquery = query.Where(\"status_code >= ? AND status_code < ?\", 200, 400)\n\t\tcase \"failed\":\n\t\t\tquery = query.Where(\"status_code < ? OR status_code >= ?\", 200, 400)\n\t\t}\n\t}\n\n\tif filter.StatusCode != nil {\n\t\tquery = query.Where(\"status_code = ?\", *filter.StatusCode)\n\t}\n\n\tif filter.StartDate != nil {\n\t\tquery = query.Where(\"timestamp >= ?\", *filter.StartDate)\n\t}\n\n\tif filter.EndDate != nil {\n\t\tquery = query.Where(\"timestamp <= ?\", *filter.EndDate)\n\t}\n\n\treturn query\n}\n\nfunc (s *SqlStore) GetServiceTargetByTargetID(ctx context.Context, lockStrength LockingStrength, accountID string, targetID string) (*reverseproxy.Target, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar target *reverseproxy.Target\n\tresult := tx.Take(&target, \"account_id = ? AND target_id = ?\", accountID, targetID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"service target with ID %s not found\", targetID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get service target from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get service target from store\")\n\t}\n\n\treturn target, nil\n}","newString":"package store\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n\t\"github.com/rs/xid\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gorm.io/driver/mysql\"\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/driver/sqlite\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n\t\"gorm.io/gorm/logger\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/management/internals/modules/reverseproxy\"\n\t\"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs\"\n\t\"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain\"\n\t\"github.com/netbirdio/netbird/management/internals/modules/zones\"\n\t\"github.com/netbirdio/netbird/management/internals/modules/zones/records\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/telemetry\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/management/server/util\"\n\t\"github.com/netbirdio/netbird/route\"\n\t\"github.com/netbirdio/netbird/shared/management/status\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n)\n\nconst (\n\tstoreSqliteFileName = \"store.db\"\n\tidQueryCondition = \"id = ?\"\n\tkeyQueryCondition = \"key = ?\"\n\tmysqlKeyQueryCondition = \"`key` = ?\"\n\taccountAndIDQueryCondition = \"account_id = ? and id = ?\"\n\taccountAndPeerIDQueryCondition = \"account_id = ? and peer_id = ?\"\n\taccountAndIDsQueryCondition = \"account_id = ? AND id IN ?\"\n\taccountIDCondition = \"account_id = ?\"\n\tpeerNotFoundFMT = \"peer %s not found\"\n\n\tpgMaxConnections = 30\n\tpgMinConnections = 1\n\tpgMaxConnLifetime = 60 * time.Minute\n\tpgHealthCheckPeriod = 1 * time.Minute\n)\n\n// SqlStore represents an account storage backed by a Sql DB persisted to disk\ntype SqlStore struct {\n\tdb *gorm.DB\n\tglobalAccountLock sync.Mutex\n\tmetrics telemetry.AppMetrics\n\tinstallationPK int\n\tstoreEngine types.Engine\n\tpool *pgxpool.Pool\n\tfieldEncrypt *crypt.FieldEncrypt\n\ttransactionTimeout time.Duration\n}\n\ntype installation struct {\n\tID uint `gorm:\"primaryKey\"`\n\tInstallationIDValue string\n}\n\ntype migrationFunc func(*gorm.DB) error\n\n// NewSqlStore creates a new SqlStore instance.\nfunc NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tsql, err := db.DB()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconns, err := strconv.Atoi(os.Getenv(\"NB_SQL_MAX_OPEN_CONNS\"))\n\tif err != nil {\n\t\tconns = runtime.NumCPU()\n\t}\n\n\ttransactionTimeout := 5 * time.Minute\n\tif v := os.Getenv(\"NB_STORE_TRANSACTION_TIMEOUT\"); v != \"\" {\n\t\tif parsed, err := time.ParseDuration(v); err == nil {\n\t\t\ttransactionTimeout = parsed\n\t\t}\n\t}\n\tlog.WithContext(ctx).Infof(\"Setting transaction timeout to %v\", transactionTimeout)\n\n\tif storeEngine == types.SqliteStoreEngine {\n\t\tif err == nil {\n\t\t\tlog.WithContext(ctx).Warnf(\"setting NB_SQL_MAX_OPEN_CONNS is not supported for sqlite, using default value 1\")\n\t\t}\n\t\tconns = 1\n\t}\n\n\tsql.SetMaxOpenConns(conns)\n\tsql.SetMaxIdleConns(conns)\n\tsql.SetConnMaxLifetime(time.Hour)\n\tsql.SetConnMaxIdleTime(3 * time.Minute)\n\n\tlog.WithContext(ctx).Infof(\"Set max open db connections to %d, max idle to %d, max lifetime to %v, max idle time to %v\",\n\t\tconns, conns, time.Hour, 3*time.Minute)\n\n\tif skipMigration {\n\t\tlog.WithContext(ctx).Infof(\"skipping migration\")\n\t\treturn &SqlStore{db: db, storeEngine: storeEngine, metrics: metrics, installationPK: 1, transactionTimeout: transactionTimeout}, nil\n\t}\n\n\tif err := migratePreAuto(ctx, db); err != nil {\n\t\treturn nil, fmt.Errorf(\"migratePreAuto: %w\", err)\n\t}\n\terr = db.AutoMigrate(\n\t\t&types.SetupKey{}, &nbpeer.Peer{}, &types.User{}, &types.PersonalAccessToken{}, &types.ProxyAccessToken{},\n\t\t&types.Group{}, &types.GroupPeer{},\n\t\t&types.Account{}, &types.Policy{}, &types.PolicyRule{}, &route.Route{}, &nbdns.NameServerGroup{},\n\t\t&installation{}, &types.ExtraSettings{}, &posture.Checks{}, &nbpeer.NetworkAddress{},\n\t\t&networkTypes.Network{}, &routerTypes.NetworkRouter{}, &resourceTypes.NetworkResource{}, &types.AccountOnboarding{},\n\t\t&types.Job{}, &zones.Zone{}, &records.Record{}, &types.UserInviteRecord{}, &reverseproxy.Service{}, &reverseproxy.Target{}, &domain.Domain{},\n\t\t&accesslogs.AccessLogEntry{},\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"auto migratePreAuto: %w\", err)\n\t}\n\tif err := migratePostAuto(ctx, db); err != nil {\n\t\treturn nil, fmt.Errorf(\"migratePostAuto: %w\", err)\n\t}\n\n\treturn &SqlStore{db: db, storeEngine: storeEngine, metrics: metrics, installationPK: 1, transactionTimeout: transactionTimeout}, nil\n}\n\nfunc GetKeyQueryCondition(s *SqlStore) string {\n\tif s.storeEngine == types.MysqlStoreEngine {\n\t\treturn mysqlKeyQueryCondition\n\t}\n\treturn keyQueryCondition\n}\n\n// SaveJob persists a job in DB\nfunc (s *SqlStore) CreatePeerJob(ctx context.Context, job *types.Job) error {\n\tresult := s.db.Create(job)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create job in store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create job in store\")\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) CompletePeerJob(ctx context.Context, job *types.Job) error {\n\tresult := s.db.\n\t\tModel(&types.Job{}).\n\t\tWhere(idQueryCondition, job.ID).\n\t\tUpdates(job)\n\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update job in store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to update job in store\")\n\t}\n\treturn nil\n}\n\n// job was pending for too long and has been cancelled\nfunc (s *SqlStore) MarkPendingJobsAsFailed(ctx context.Context, accountID, peerID, jobID, reason string) error {\n\tnow := time.Now().UTC()\n\tresult := s.db.\n\t\tModel(&types.Job{}).\n\t\tWhere(accountAndPeerIDQueryCondition+\" AND id = ?\"+\" AND status = ?\", accountID, peerID, jobID, types.JobStatusPending).\n\t\tUpdates(types.Job{\n\t\t\tStatus: types.JobStatusFailed,\n\t\t\tFailedReason: reason,\n\t\t\tCompletedAt: &now,\n\t\t})\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to mark pending jobs as Failed job in store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to mark pending job as Failed in store\")\n\t}\n\treturn nil\n}\n\n// job was pending for too long and has been cancelled\nfunc (s *SqlStore) MarkAllPendingJobsAsFailed(ctx context.Context, accountID, peerID, reason string) error {\n\tnow := time.Now().UTC()\n\tresult := s.db.\n\t\tModel(&types.Job{}).\n\t\tWhere(accountAndPeerIDQueryCondition+\" AND status = ?\", accountID, peerID, types.JobStatusPending).\n\t\tUpdates(types.Job{\n\t\t\tStatus: types.JobStatusFailed,\n\t\t\tFailedReason: reason,\n\t\t\tCompletedAt: &now,\n\t\t})\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to mark pending jobs as Failed job in store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to mark pending job as Failed in store\")\n\t}\n\treturn nil\n}\n\n// GetJobByID fetches job by ID\nfunc (s *SqlStore) GetPeerJobByID(ctx context.Context, accountID, jobID string) (*types.Job, error) {\n\tvar job types.Job\n\terr := s.db.\n\t\tWhere(accountAndIDQueryCondition, accountID, jobID).\n\t\tFirst(&job).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\treturn nil, status.Errorf(status.NotFound, \"job %s not found\", jobID)\n\t}\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to fetch job from store: %s\", err)\n\t\treturn nil, err\n\t}\n\treturn &job, nil\n}\n\n// get all jobs\nfunc (s *SqlStore) GetPeerJobs(ctx context.Context, accountID, peerID string) ([]*types.Job, error) {\n\tvar jobs []*types.Job\n\terr := s.db.\n\t\tWhere(accountAndPeerIDQueryCondition, accountID, peerID).\n\t\tOrder(\"created_at DESC\").\n\t\tFind(&jobs).Error\n\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to fetch jobs from store: %s\", err)\n\t\treturn nil, err\n\t}\n\n\treturn jobs, nil\n}\n\n// AcquireGlobalLock acquires global lock across all the accounts and returns a function that releases the lock\nfunc (s *SqlStore) AcquireGlobalLock(ctx context.Context) (unlock func()) {\n\tlog.WithContext(ctx).Tracef(\"acquiring global lock\")\n\tstart := time.Now()\n\ts.globalAccountLock.Lock()\n\n\tunlock = func() {\n\t\ts.globalAccountLock.Unlock()\n\t\tlog.WithContext(ctx).Tracef(\"released global lock in %v\", time.Since(start))\n\t}\n\n\ttook := time.Since(start)\n\tlog.WithContext(ctx).Tracef(\"took %v to acquire global lock\", took)\n\tif s.metrics != nil {\n\t\ts.metrics.StoreMetrics().CountGlobalLockAcquisitionDuration(took)\n\t}\n\n\treturn unlock\n}\n\n// Deprecated: Full account operations are no longer supported\nfunc (s *SqlStore) SaveAccount(ctx context.Context, account *types.Account) error {\n\tstart := time.Now()\n\tdefer func() {\n\t\telapsed := time.Since(start)\n\t\tif elapsed > 1*time.Second {\n\t\t\tlog.WithContext(ctx).Tracef(\"SaveAccount for account %s exceeded 1s, took: %v\", account.Id, elapsed)\n\t\t}\n\t}()\n\n\t// todo: remove this check after the issue is resolved\n\ts.checkAccountDomainBeforeSave(ctx, account.Id, account.Domain)\n\n\tgenerateAccountSQLTypes(account)\n\n\t// Encrypt sensitive user data before saving\n\tfor i := range account.UsersG {\n\t\tif err := account.UsersG[i].EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn fmt.Errorf(\"encrypt user: %w\", err)\n\t\t}\n\t}\n\n\tfor _, group := range account.GroupsG {\n\t\tgroup.StoreGroupPeers()\n\t}\n\n\terr := s.transaction(func(tx *gorm.DB) error {\n\t\tresult := tx.Select(clause.Associations).Delete(account.Policies, \"account_id = ?\", account.Id)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\tresult = tx.Select(clause.Associations).Delete(account.UsersG, \"account_id = ?\", account.Id)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\tresult = tx.Select(clause.Associations).Delete(account)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\tresult = tx.\n\t\t\tSession(&gorm.Session{FullSaveAssociations: true}).\n\t\t\tClauses(clause.OnConflict{UpdateAll: true}).\n\t\t\tCreate(account)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\t\treturn nil\n\t})\n\n\ttook := time.Since(start)\n\tif s.metrics != nil {\n\t\ts.metrics.StoreMetrics().CountPersistenceDuration(took)\n\t}\n\tlog.WithContext(ctx).Debugf(\"took %d ms to persist an account to the store\", took.Milliseconds())\n\n\treturn err\n}\n\n// generateAccountSQLTypes generates the GORM compatible types for the account\nfunc generateAccountSQLTypes(account *types.Account) {\n\tfor _, key := range account.SetupKeys {\n\t\taccount.SetupKeysG = append(account.SetupKeysG, *key)\n\t}\n\n\tif len(account.SetupKeys) != len(account.SetupKeysG) {\n\t\tlog.Warnf(\"SetupKeysG length mismatch for account %s\", account.Id)\n\t}\n\n\tfor id, peer := range account.Peers {\n\t\tpeer.ID = id\n\t\taccount.PeersG = append(account.PeersG, *peer)\n\t}\n\n\tfor id, user := range account.Users {\n\t\tuser.Id = id\n\t\tfor id, pat := range user.PATs {\n\t\t\tpat.ID = id\n\t\t\tuser.PATsG = append(user.PATsG, *pat)\n\t\t}\n\t\taccount.UsersG = append(account.UsersG, *user)\n\t}\n\n\tfor id, group := range account.Groups {\n\t\tgroup.ID = id\n\t\tgroup.AccountID = account.Id\n\t\taccount.GroupsG = append(account.GroupsG, group)\n\t}\n\n\tfor id, route := range account.Routes {\n\t\troute.ID = id\n\t\taccount.RoutesG = append(account.RoutesG, *route)\n\t}\n\n\tfor id, ns := range account.NameServerGroups {\n\t\tns.ID = id\n\t\taccount.NameServerGroupsG = append(account.NameServerGroupsG, *ns)\n\t}\n}\n\n// checkAccountDomainBeforeSave temporary method to troubleshoot an issue with domains getting blank\nfunc (s *SqlStore) checkAccountDomainBeforeSave(ctx context.Context, accountID, newDomain string) {\n\tvar acc types.Account\n\tvar domain string\n\tresult := s.db.Model(&acc).Select(\"domain\").Where(idQueryCondition, accountID).Take(&domain)\n\tif result.Error != nil {\n\t\tif !errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\tlog.WithContext(ctx).Errorf(\"error when getting account %s from the store to check domain: %s\", accountID, result.Error)\n\t\t}\n\t\treturn\n\t}\n\tif domain != \"\" && newDomain == \"\" {\n\t\tlog.WithContext(ctx).Warnf(\"saving an account with empty domain when there was a domain set. Previous domain %s, Account ID: %s, Trace: %s\", domain, accountID, debug.Stack())\n\t}\n}\n\nfunc (s *SqlStore) DeleteAccount(ctx context.Context, account *types.Account) error {\n\tstart := time.Now()\n\n\terr := s.transaction(func(tx *gorm.DB) error {\n\t\tresult := tx.Select(clause.Associations).Delete(account.Policies, \"account_id = ?\", account.Id)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\tresult = tx.Select(clause.Associations).Delete(account.UsersG, \"account_id = ?\", account.Id)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\tresult = tx.Select(clause.Associations).Delete(account)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\treturn nil\n\t})\n\n\ttook := time.Since(start)\n\tif s.metrics != nil {\n\t\ts.metrics.StoreMetrics().CountPersistenceDuration(took)\n\t}\n\tlog.WithContext(ctx).Tracef(\"took %d ms to delete an account to the store\", took.Milliseconds())\n\n\treturn err\n}\n\nfunc (s *SqlStore) SaveInstallationID(_ context.Context, ID string) error {\n\tinstallation := installation{InstallationIDValue: ID}\n\tinstallation.ID = uint(s.installationPK)\n\n\treturn s.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&installation).Error\n}\n\nfunc (s *SqlStore) GetInstallationID() string {\n\tvar installation installation\n\n\tif result := s.db.Take(&installation, idQueryCondition, s.installationPK); result.Error != nil {\n\t\treturn \"\"\n\t}\n\n\treturn installation.InstallationIDValue\n}\n\nfunc (s *SqlStore) SavePeer(ctx context.Context, accountID string, peer *nbpeer.Peer) error {\n\t// To maintain data integrity, we create a copy of the peer's to prevent unintended updates to other fields.\n\tpeerCopy := peer.Copy()\n\tpeerCopy.AccountID = accountID\n\n\terr := s.transaction(func(tx *gorm.DB) error {\n\t\t// check if peer exists before saving\n\t\tvar peerID string\n\t\tresult := tx.Model(&nbpeer.Peer{}).Select(\"id\").Take(&peerID, accountAndIDQueryCondition, accountID, peer.ID)\n\t\tif result.Error != nil {\n\t\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\t\treturn status.Errorf(status.NotFound, peerNotFoundFMT, peer.ID)\n\t\t\t}\n\t\t\treturn result.Error\n\t\t}\n\n\t\tif peerID == \"\" {\n\t\t\treturn status.Errorf(status.NotFound, peerNotFoundFMT, peer.ID)\n\t\t}\n\n\t\tresult = tx.Model(&nbpeer.Peer{}).Where(accountAndIDQueryCondition, accountID, peer.ID).Save(peerCopy)\n\t\tif result.Error != nil {\n\t\t\treturn status.Errorf(status.Internal, \"failed to save peer to store: %v\", result.Error)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) UpdateAccountDomainAttributes(ctx context.Context, accountID string, domain string, category string, isPrimaryDomain bool) error {\n\taccountCopy := types.Account{\n\t\tDomain: domain,\n\t\tDomainCategory: category,\n\t\tIsDomainPrimaryAccount: isPrimaryDomain,\n\t}\n\n\tfieldsToUpdate := []string{\"domain\", \"domain_category\", \"is_domain_primary_account\"}\n\tresult := s.db.Model(&types.Account{}).\n\t\tSelect(fieldsToUpdate).\n\t\tWhere(idQueryCondition, accountID).\n\t\tUpdates(&accountCopy)\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"failed to update account domain attributes to store: %v\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"account %s\", accountID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) SavePeerStatus(ctx context.Context, accountID, peerID string, peerStatus nbpeer.PeerStatus) error {\n\tvar peerCopy nbpeer.Peer\n\tpeerCopy.Status = &peerStatus\n\n\tfieldsToUpdate := []string{\n\t\t\"peer_status_last_seen\", \"peer_status_connected\",\n\t\t\"peer_status_login_expired\", \"peer_status_required_approval\",\n\t}\n\tresult := s.db.Model(&nbpeer.Peer{}).\n\t\tSelect(fieldsToUpdate).\n\t\tWhere(accountAndIDQueryCondition, accountID, peerID).\n\t\tUpdates(&peerCopy)\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"failed to save peer status to store: %v\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, peerNotFoundFMT, peerID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) SavePeerLocation(ctx context.Context, accountID string, peerWithLocation *nbpeer.Peer) error {\n\t// To maintain data integrity, we create a copy of the peer's location to prevent unintended updates to other fields.\n\tvar peerCopy nbpeer.Peer\n\t// Since the location field has been migrated to JSON serialization,\n\t// updating the struct ensures the correct data format is inserted into the database.\n\tpeerCopy.Location = peerWithLocation.Location\n\n\tresult := s.db.Model(&nbpeer.Peer{}).\n\t\tWhere(accountAndIDQueryCondition, accountID, peerWithLocation.ID).\n\t\tUpdates(peerCopy)\n\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"failed to save peer locations to store: %v\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, peerNotFoundFMT, peerWithLocation.ID)\n\t}\n\n\treturn nil\n}\n\n// ApproveAccountPeers marks all peers that currently require approval in the given account as approved.\nfunc (s *SqlStore) ApproveAccountPeers(ctx context.Context, accountID string) (int, error) {\n\tresult := s.db.Model(&nbpeer.Peer{}).\n\t\tWhere(\"account_id = ? AND peer_status_requires_approval = ?\", accountID, true).\n\t\tUpdate(\"peer_status_requires_approval\", false)\n\tif result.Error != nil {\n\t\treturn 0, status.Errorf(status.Internal, \"failed to approve pending account peers: %v\", result.Error)\n\t}\n\n\treturn int(result.RowsAffected), nil\n}\n\n// SaveUsers saves the given list of users to the database.\nfunc (s *SqlStore) SaveUsers(ctx context.Context, users []*types.User) error {\n\tif len(users) == 0 {\n\t\treturn nil\n\t}\n\n\tusersCopy := make([]*types.User, len(users))\n\tfor i, user := range users {\n\t\tuserCopy := user.Copy()\n\t\tuserCopy.Email = user.Email\n\t\tuserCopy.Name = user.Name\n\t\tif err := userCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn fmt.Errorf(\"encrypt user: %w\", err)\n\t\t}\n\t\tusersCopy[i] = userCopy\n\t}\n\n\tresult := s.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&usersCopy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save users to store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save users to store\")\n\t}\n\treturn nil\n}\n\n// SaveUser saves the given user to the database.\nfunc (s *SqlStore) SaveUser(ctx context.Context, user *types.User) error {\n\tuserCopy := user.Copy()\n\tuserCopy.Email = user.Email\n\tuserCopy.Name = user.Name\n\n\tif err := userCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn fmt.Errorf(\"encrypt user: %w\", err)\n\t}\n\n\tresult := s.db.Save(userCopy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save user to store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save user to store\")\n\t}\n\treturn nil\n}\n\n// CreateGroups creates the given list of groups to the database.\nfunc (s *SqlStore) CreateGroups(ctx context.Context, accountID string, groups []*types.Group) error {\n\tif len(groups) == 0 {\n\t\treturn nil\n\t}\n\n\treturn s.db.Transaction(func(tx *gorm.DB) error {\n\t\tresult := tx.\n\t\t\tClauses(\n\t\t\t\tclause.OnConflict{\n\t\t\t\t\tWhere: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: \"groups.account_id\", Value: accountID}}},\n\t\t\t\t\tUpdateAll: true,\n\t\t\t\t},\n\t\t\t).\n\t\t\tOmit(clause.Associations).\n\t\t\tCreate(&groups)\n\t\tif result.Error != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"failed to save groups to store: %v\", result.Error)\n\t\t\treturn status.Errorf(status.Internal, \"failed to save groups to store\")\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\n// UpdateGroups updates the given list of groups to the database.\nfunc (s *SqlStore) UpdateGroups(ctx context.Context, accountID string, groups []*types.Group) error {\n\tif len(groups) == 0 {\n\t\treturn nil\n\t}\n\n\treturn s.db.Transaction(func(tx *gorm.DB) error {\n\t\tresult := tx.\n\t\t\tClauses(\n\t\t\t\tclause.OnConflict{\n\t\t\t\t\tWhere: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: \"groups.account_id\", Value: accountID}}},\n\t\t\t\t\tUpdateAll: true,\n\t\t\t\t},\n\t\t\t).\n\t\t\tOmit(clause.Associations).\n\t\t\tCreate(&groups)\n\t\tif result.Error != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"failed to save groups to store: %v\", result.Error)\n\t\t\treturn status.Errorf(status.Internal, \"failed to save groups to store\")\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\n// DeleteHashedPAT2TokenIDIndex is noop in SqlStore\nfunc (s *SqlStore) DeleteHashedPAT2TokenIDIndex(hashedToken string) error {\n\treturn nil\n}\n\n// DeleteTokenID2UserIDIndex is noop in SqlStore\nfunc (s *SqlStore) DeleteTokenID2UserIDIndex(tokenID string) error {\n\treturn nil\n}\n\nfunc (s *SqlStore) GetAccountByPrivateDomain(ctx context.Context, domain string) (*types.Account, error) {\n\taccountID, err := s.GetAccountIDByPrivateDomain(ctx, LockingStrengthNone, domain)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// TODO: rework to not call GetAccount\n\treturn s.GetAccount(ctx, accountID)\n}\n\nfunc (s *SqlStore) GetAccountIDByPrivateDomain(ctx context.Context, lockStrength LockingStrength, domain string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountID string\n\tresult := tx.Model(&types.Account{}).Select(\"id\").\n\t\tWhere(\"domain = ? and is_domain_primary_account = ? and domain_category = ?\",\n\t\t\tstrings.ToLower(domain), true, types.PrivateCategory,\n\t\t).Take(&accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: provided domain is not registered or is not private\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"error when getting account from the store: %s\", result.Error)\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn accountID, nil\n}\n\nfunc (s *SqlStore) GetAccountBySetupKey(ctx context.Context, setupKey string) (*types.Account, error) {\n\tvar key types.SetupKey\n\tresult := s.db.Select(\"account_id\").Take(&key, GetKeyQueryCondition(s), setupKey)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewSetupKeyNotFoundError(setupKey)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get account by setup key from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get account by setup key from store\")\n\t}\n\n\tif key.AccountID == \"\" {\n\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn s.GetAccount(ctx, key.AccountID)\n}\n\nfunc (s *SqlStore) GetTokenIDByHashedToken(ctx context.Context, hashedToken string) (string, error) {\n\tvar token types.PersonalAccessToken\n\tresult := s.db.Take(&token, \"hashed_token = ?\", hashedToken)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"error when getting token from the store: %s\", result.Error)\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn token.ID, nil\n}\n\nfunc (s *SqlStore) GetUserByPATID(ctx context.Context, lockStrength LockingStrength, patID string) (*types.User, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar user types.User\n\tresult := tx.\n\t\tJoins(\"JOIN personal_access_tokens ON personal_access_tokens.user_id = users.id\").\n\t\tWhere(\"personal_access_tokens.id = ?\", patID).Take(&user)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPATNotFoundError(patID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get token user from the store: %s\", result.Error)\n\t\treturn nil, status.NewGetUserFromStoreError()\n\t}\n\n\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t}\n\n\treturn &user, nil\n}\n\nfunc (s *SqlStore) GetUserByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (*types.User, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar user types.User\n\tresult := tx.Take(&user, idQueryCondition, userID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewUserNotFoundError(userID)\n\t\t}\n\t\treturn nil, status.NewGetUserFromStoreError()\n\t}\n\n\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t}\n\n\treturn &user, nil\n}\n\nfunc (s *SqlStore) DeleteUser(ctx context.Context, accountID, userID string) error {\n\terr := s.transaction(func(tx *gorm.DB) error {\n\t\tresult := tx.Delete(&types.PersonalAccessToken{}, \"user_id = ?\", userID)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\treturn tx.Delete(&types.User{}, accountAndIDQueryCondition, accountID, userID).Error\n\t})\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete user from the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to delete user from store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetAccountUsers(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.User, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar users []*types.User\n\tresult := tx.Find(&users, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"accountID not found: index lookup failed\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"error when getting users from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting users from store\")\n\t}\n\n\tfor _, user := range users {\n\t\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t\t}\n\t}\n\n\treturn users, nil\n}\n\nfunc (s *SqlStore) GetAccountOwner(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.User, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar user types.User\n\tresult := tx.Take(&user, \"account_id = ? AND role = ?\", accountID, types.UserRoleOwner)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"account owner not found: index lookup failed\")\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get account owner from the store\")\n\t}\n\n\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t}\n\n\treturn &user, nil\n}\n\n// SaveUserInvite saves a user invite to the database\nfunc (s *SqlStore) SaveUserInvite(ctx context.Context, invite *types.UserInviteRecord) error {\n\tinviteCopy := invite.Copy()\n\tif err := inviteCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn fmt.Errorf(\"encrypt invite: %w\", err)\n\t}\n\n\tresult := s.db.Save(inviteCopy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save user invite to store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save user invite to store\")\n\t}\n\treturn nil\n}\n\n// GetUserInviteByID retrieves a user invite by its ID and account ID\nfunc (s *SqlStore) GetUserInviteByID(ctx context.Context, lockStrength LockingStrength, accountID, inviteID string) (*types.UserInviteRecord, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar invite types.UserInviteRecord\n\tresult := tx.Where(\"account_id = ?\", accountID).Take(&invite, idQueryCondition, inviteID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"user invite not found\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get user invite from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get user invite from store\")\n\t}\n\n\tif err := invite.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt invite: %w\", err)\n\t}\n\n\treturn &invite, nil\n}\n\n// GetUserInviteByHashedToken retrieves a user invite by its hashed token\nfunc (s *SqlStore) GetUserInviteByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken string) (*types.UserInviteRecord, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar invite types.UserInviteRecord\n\tresult := tx.Take(&invite, \"hashed_token = ?\", hashedToken)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"user invite not found\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get user invite from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get user invite from store\")\n\t}\n\n\tif err := invite.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt invite: %w\", err)\n\t}\n\n\treturn &invite, nil\n}\n\n// GetUserInviteByEmail retrieves a user invite by account ID and email.\n// Since email is encrypted with random IVs, we fetch all invites for the account\n// and compare emails in memory after decryption.\nfunc (s *SqlStore) GetUserInviteByEmail(ctx context.Context, lockStrength LockingStrength, accountID, email string) (*types.UserInviteRecord, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar invites []*types.UserInviteRecord\n\tresult := tx.Find(&invites, \"account_id = ?\", accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get user invites from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get user invites from store\")\n\t}\n\n\tfor _, invite := range invites {\n\t\tif err := invite.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt invite: %w\", err)\n\t\t}\n\t\tif strings.EqualFold(invite.Email, email) {\n\t\t\treturn invite, nil\n\t\t}\n\t}\n\n\treturn nil, status.Errorf(status.NotFound, \"user invite not found for email\")\n}\n\n// GetAccountUserInvites retrieves all user invites for an account\nfunc (s *SqlStore) GetAccountUserInvites(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.UserInviteRecord, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar invites []*types.UserInviteRecord\n\tresult := tx.Find(&invites, \"account_id = ?\", accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get user invites from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get user invites from store\")\n\t}\n\n\tfor _, invite := range invites {\n\t\tif err := invite.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt invite: %w\", err)\n\t\t}\n\t}\n\n\treturn invites, nil\n}\n\n// DeleteUserInvite deletes a user invite by its ID\nfunc (s *SqlStore) DeleteUserInvite(ctx context.Context, inviteID string) error {\n\tresult := s.db.Delete(&types.UserInviteRecord{}, idQueryCondition, inviteID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete user invite from store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete user invite from store\")\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) GetAccountGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.Group, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar groups []*types.Group\n\tresult := tx.Preload(clause.Associations).Find(&groups, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"accountID not found: index lookup failed\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get account groups from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get account groups from the store\")\n\t}\n\n\tfor _, g := range groups {\n\t\tg.LoadGroupPeers()\n\t}\n\n\treturn groups, nil\n}\n\nfunc (s *SqlStore) GetResourceGroups(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) ([]*types.Group, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar groups []*types.Group\n\n\tlikePattern := `%\"ID\":\"` + resourceID + `\"%`\n\n\tresult := tx.\n\t\tPreload(clause.Associations).\n\t\tWhere(\"resources LIKE ?\", likePattern).\n\t\tFind(&groups)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, result.Error\n\t}\n\n\tfor _, g := range groups {\n\t\tg.LoadGroupPeers()\n\t}\n\n\treturn groups, nil\n}\n\nfunc (s *SqlStore) GetAccountsCounter(ctx context.Context) (int64, error) {\n\tvar count int64\n\tresult := s.db.Model(&types.Account{}).Count(&count)\n\tif result.Error != nil {\n\t\treturn 0, fmt.Errorf(\"failed to get all accounts counter: %w\", result.Error)\n\t}\n\n\treturn count, nil\n}\n\n// GetCustomDomainsCounts returns the total and validated custom domain counts.\nfunc (s *SqlStore) GetCustomDomainsCounts(ctx context.Context) (int64, int64, error) {\n\tvar total, validated int64\n\tif err := s.db.WithContext(ctx).Model(&domain.Domain{}).Count(&total).Error; err != nil {\n\t\treturn 0, 0, err\n\t}\n\tif err := s.db.WithContext(ctx).Model(&domain.Domain{}).Where(\"validated = ?\", true).Count(&validated).Error; err != nil {\n\t\treturn 0, 0, err\n\t}\n\treturn total, validated, nil\n}\n\nfunc (s *SqlStore) GetAllAccounts(ctx context.Context) (all []*types.Account) {\n\tvar accounts []types.Account\n\tresult := s.db.Find(&accounts)\n\tif result.Error != nil {\n\t\treturn all\n\t}\n\n\tfor _, account := range accounts {\n\t\tif acc, err := s.GetAccount(ctx, account.Id); err == nil {\n\t\t\tall = append(all, acc)\n\t\t}\n\t}\n\n\treturn all\n}\n\nfunc (s *SqlStore) GetAccountMeta(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.AccountMeta, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountMeta types.AccountMeta\n\tresult := tx.Model(&types.Account{}).\n\t\tTake(&accountMeta, idQueryCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"error when getting account meta %s from the store: %s\", accountID, result.Error)\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn &accountMeta, nil\n}\n\n// GetAccountOnboarding retrieves the onboarding information for a specific account.\nfunc (s *SqlStore) GetAccountOnboarding(ctx context.Context, accountID string) (*types.AccountOnboarding, error) {\n\tvar accountOnboarding types.AccountOnboarding\n\tresult := s.db.Model(&accountOnboarding).Take(&accountOnboarding, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewAccountOnboardingNotFoundError(accountID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"error when getting account onboarding %s from the store: %s\", accountID, result.Error)\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn &accountOnboarding, nil\n}\n\n// SaveAccountOnboarding updates the onboarding information for a specific account.\nfunc (s *SqlStore) SaveAccountOnboarding(ctx context.Context, onboarding *types.AccountOnboarding) error {\n\tresult := s.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(onboarding)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"error when saving account onboarding %s in the store: %s\", onboarding.AccountID, result.Error)\n\t\treturn status.Errorf(status.Internal, \"error when saving account onboarding %s in the store: %s\", onboarding.AccountID, result.Error)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetAccount(ctx context.Context, accountID string) (*types.Account, error) {\n\tif s.pool != nil {\n\t\treturn s.getAccountPgx(ctx, accountID)\n\t}\n\treturn s.getAccountGorm(ctx, accountID)\n}\n\nfunc (s *SqlStore) getAccountGorm(ctx context.Context, accountID string) (*types.Account, error) {\n\tstart := time.Now()\n\tdefer func() {\n\t\telapsed := time.Since(start)\n\t\tif elapsed > 1*time.Second {\n\t\t\tlog.WithContext(ctx).Tracef(\"GetAccount for account %s exceeded 1s, took: %v\", accountID, elapsed)\n\t\t}\n\t}()\n\n\tvar account types.Account\n\tresult := s.db.Model(&account).\n\t\tPreload(\"UsersG.PATsG\"). // have to be specified as this is nested reference\n\t\tPreload(\"Policies.Rules\").\n\t\tPreload(\"SetupKeysG\").\n\t\tPreload(\"PeersG\").\n\t\tPreload(\"UsersG\").\n\t\tPreload(\"GroupsG.GroupPeers\").\n\t\tPreload(\"RoutesG\").\n\t\tPreload(\"NameServerGroupsG\").\n\t\tPreload(\"PostureChecks\").\n\t\tPreload(\"Networks\").\n\t\tPreload(\"NetworkRouters\").\n\t\tPreload(\"NetworkResources\").\n\t\tPreload(\"Onboarding\").\n\t\tPreload(\"Services.Targets\").\n\t\tTake(&account, idQueryCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"error when getting account %s from the store: %s\", accountID, result.Error)\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\taccount.SetupKeys = make(map[string]*types.SetupKey, len(account.SetupKeysG))\n\tfor _, key := range account.SetupKeysG {\n\t\tif key.UpdatedAt.IsZero() {\n\t\t\tkey.UpdatedAt = key.CreatedAt\n\t\t}\n\t\tif key.AutoGroups == nil {\n\t\t\tkey.AutoGroups = []string{}\n\t\t}\n\t\taccount.SetupKeys[key.Key] = &key\n\t}\n\taccount.SetupKeysG = nil\n\n\taccount.Peers = make(map[string]*nbpeer.Peer, len(account.PeersG))\n\tfor _, peer := range account.PeersG {\n\t\taccount.Peers[peer.ID] = &peer\n\t}\n\taccount.PeersG = nil\n\taccount.Users = make(map[string]*types.User, len(account.UsersG))\n\tfor _, user := range account.UsersG {\n\t\tuser.PATs = make(map[string]*types.PersonalAccessToken, len(user.PATs))\n\t\tfor _, pat := range user.PATsG {\n\t\t\tpat.UserID = \"\"\n\t\t\tuser.PATs[pat.ID] = &pat\n\t\t}\n\t\tif user.AutoGroups == nil {\n\t\t\tuser.AutoGroups = []string{}\n\t\t}\n\t\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t\t}\n\t\taccount.Users[user.Id] = &user\n\t\tuser.PATsG = nil\n\t}\n\taccount.UsersG = nil\n\taccount.Groups = make(map[string]*types.Group, len(account.GroupsG))\n\tfor _, group := range account.GroupsG {\n\t\tgroup.Peers = make([]string, len(group.GroupPeers))\n\t\tfor i, gp := range group.GroupPeers {\n\t\t\tgroup.Peers[i] = gp.PeerID\n\t\t}\n\t\tif group.Resources == nil {\n\t\t\tgroup.Resources = []types.Resource{}\n\t\t}\n\t\taccount.Groups[group.ID] = group\n\t}\n\taccount.GroupsG = nil\n\n\taccount.Routes = make(map[route.ID]*route.Route, len(account.RoutesG))\n\tfor _, route := range account.RoutesG {\n\t\taccount.Routes[route.ID] = &route\n\t}\n\taccount.RoutesG = nil\n\taccount.NameServerGroups = make(map[string]*nbdns.NameServerGroup, len(account.NameServerGroupsG))\n\tfor _, ns := range account.NameServerGroupsG {\n\t\tns.AccountID = \"\"\n\t\tif ns.NameServers == nil {\n\t\t\tns.NameServers = []nbdns.NameServer{}\n\t\t}\n\t\tif ns.Groups == nil {\n\t\t\tns.Groups = []string{}\n\t\t}\n\t\tif ns.Domains == nil {\n\t\t\tns.Domains = []string{}\n\t\t}\n\t\taccount.NameServerGroups[ns.ID] = &ns\n\t}\n\taccount.NameServerGroupsG = nil\n\taccount.InitOnce()\n\treturn &account, nil\n}\n\nfunc (s *SqlStore) getAccountPgx(ctx context.Context, accountID string) (*types.Account, error) {\n\taccount, err := s.getAccount(ctx, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar wg sync.WaitGroup\n\terrChan := make(chan error, 12)\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tkeys, err := s.getSetupKeys(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.SetupKeysG = keys\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tpeers, err := s.getPeers(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.PeersG = peers\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tusers, err := s.getUsers(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.UsersG = users\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tgroups, err := s.getGroups(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.GroupsG = groups\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tpolicies, err := s.getPolicies(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.Policies = policies\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\troutes, err := s.getRoutes(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.RoutesG = routes\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tnsgs, err := s.getNameServerGroups(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.NameServerGroupsG = nsgs\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tchecks, err := s.getPostureChecks(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.PostureChecks = checks\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tservices, err := s.getServices(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.Services = services\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tnetworks, err := s.getNetworks(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.Networks = networks\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\trouters, err := s.getNetworkRouters(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.NetworkRouters = routers\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tresources, err := s.getNetworkResources(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.NetworkResources = resources\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\terr := s.getAccountOnboarding(ctx, accountID, account)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t}()\n\n\twg.Wait()\n\tclose(errChan)\n\tfor e := range errChan {\n\t\tif e != nil {\n\t\t\treturn nil, e\n\t\t}\n\t}\n\n\tvar userIDs []string\n\tfor _, u := range account.UsersG {\n\t\tuserIDs = append(userIDs, u.Id)\n\t}\n\tvar policyIDs []string\n\tfor _, p := range account.Policies {\n\t\tpolicyIDs = append(policyIDs, p.ID)\n\t}\n\tvar groupIDs []string\n\tfor _, g := range account.GroupsG {\n\t\tgroupIDs = append(groupIDs, g.ID)\n\t}\n\n\twg.Add(3)\n\terrChan = make(chan error, 3)\n\n\tvar pats []types.PersonalAccessToken\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tvar err error\n\t\tpats, err = s.getPersonalAccessTokens(ctx, userIDs)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t}\n\t}()\n\n\tvar rules []*types.PolicyRule\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tvar err error\n\t\trules, err = s.getPolicyRules(ctx, policyIDs)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t}\n\t}()\n\n\tvar groupPeers []types.GroupPeer\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tvar err error\n\t\tgroupPeers, err = s.getGroupPeers(ctx, groupIDs)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t}\n\t}()\n\n\twg.Wait()\n\tclose(errChan)\n\tfor e := range errChan {\n\t\tif e != nil {\n\t\t\treturn nil, e\n\t\t}\n\t}\n\n\tpatsByUserID := make(map[string][]*types.PersonalAccessToken)\n\tfor i := range pats {\n\t\tpat := &pats[i]\n\t\tpatsByUserID[pat.UserID] = append(patsByUserID[pat.UserID], pat)\n\t\tpat.UserID = \"\"\n\t}\n\n\trulesByPolicyID := make(map[string][]*types.PolicyRule)\n\tfor _, rule := range rules {\n\t\trulesByPolicyID[rule.PolicyID] = append(rulesByPolicyID[rule.PolicyID], rule)\n\t}\n\n\tpeersByGroupID := make(map[string][]string)\n\tfor _, gp := range groupPeers {\n\t\tpeersByGroupID[gp.GroupID] = append(peersByGroupID[gp.GroupID], gp.PeerID)\n\t}\n\n\taccount.SetupKeys = make(map[string]*types.SetupKey, len(account.SetupKeysG))\n\tfor i := range account.SetupKeysG {\n\t\tkey := &account.SetupKeysG[i]\n\t\taccount.SetupKeys[key.Key] = key\n\t}\n\n\taccount.Peers = make(map[string]*nbpeer.Peer, len(account.PeersG))\n\tfor i := range account.PeersG {\n\t\tpeer := &account.PeersG[i]\n\t\taccount.Peers[peer.ID] = peer\n\t}\n\n\taccount.Users = make(map[string]*types.User, len(account.UsersG))\n\tfor i := range account.UsersG {\n\t\tuser := &account.UsersG[i]\n\t\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t\t}\n\t\tuser.PATs = make(map[string]*types.PersonalAccessToken)\n\t\tif userPats, ok := patsByUserID[user.Id]; ok {\n\t\t\tfor j := range userPats {\n\t\t\t\tpat := userPats[j]\n\t\t\t\tuser.PATs[pat.ID] = pat\n\t\t\t}\n\t\t}\n\t\taccount.Users[user.Id] = user\n\t}\n\n\tfor i := range account.Policies {\n\t\tpolicy := account.Policies[i]\n\t\tif policyRules, ok := rulesByPolicyID[policy.ID]; ok {\n\t\t\tpolicy.Rules = policyRules\n\t\t}\n\t}\n\n\taccount.Groups = make(map[string]*types.Group, len(account.GroupsG))\n\tfor i := range account.GroupsG {\n\t\tgroup := account.GroupsG[i]\n\t\tif peerIDs, ok := peersByGroupID[group.ID]; ok {\n\t\t\tgroup.Peers = peerIDs\n\t\t}\n\t\taccount.Groups[group.ID] = group\n\t}\n\n\taccount.Routes = make(map[route.ID]*route.Route, len(account.RoutesG))\n\tfor i := range account.RoutesG {\n\t\troute := &account.RoutesG[i]\n\t\taccount.Routes[route.ID] = route\n\t}\n\n\taccount.NameServerGroups = make(map[string]*nbdns.NameServerGroup, len(account.NameServerGroupsG))\n\tfor i := range account.NameServerGroupsG {\n\t\tnsg := &account.NameServerGroupsG[i]\n\t\tnsg.AccountID = \"\"\n\t\taccount.NameServerGroups[nsg.ID] = nsg\n\t}\n\n\taccount.SetupKeysG = nil\n\taccount.PeersG = nil\n\taccount.UsersG = nil\n\taccount.GroupsG = nil\n\taccount.RoutesG = nil\n\taccount.NameServerGroupsG = nil\n\n\treturn account, nil\n}\n\nfunc (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Account, error) {\n\tvar account types.Account\n\taccount.Network = &types.Network{}\n\tconst accountQuery = `\n\t\tSELECT\n\t\t\tid, created_by, created_at, domain, domain_category, is_domain_primary_account,\n\t\t\t-- Embedded Network\n\t\t\tnetwork_identifier, network_net, network_dns, network_serial,\n\t\t\t-- Embedded DNSSettings\n\t\t\tdns_settings_disabled_management_groups,\n\t\t\t-- Embedded Settings\n\t\t\tsettings_peer_login_expiration_enabled, settings_peer_login_expiration,\n\t\t\tsettings_peer_inactivity_expiration_enabled, settings_peer_inactivity_expiration,\n\t\t\tsettings_regular_users_view_blocked, settings_groups_propagation_enabled,\n\t\t\tsettings_jwt_groups_enabled, settings_jwt_groups_claim_name, settings_jwt_allow_groups,\n\t\t\tsettings_routing_peer_dns_resolution_enabled, settings_dns_domain, settings_network_range,\n\t\t\tsettings_lazy_connection_enabled,\n\t\t\t-- Embedded ExtraSettings\n\t\t\tsettings_extra_peer_approval_enabled, settings_extra_user_approval_required,\n\t\t\tsettings_extra_integrated_validator, settings_extra_integrated_validator_groups\n\t\tFROM accounts WHERE id = $1`\n\n\tvar (\n\t\tsPeerLoginExpirationEnabled sql.NullBool\n\t\tsPeerLoginExpiration sql.NullInt64\n\t\tsPeerInactivityExpirationEnabled sql.NullBool\n\t\tsPeerInactivityExpiration sql.NullInt64\n\t\tsRegularUsersViewBlocked sql.NullBool\n\t\tsGroupsPropagationEnabled sql.NullBool\n\t\tsJWTGroupsEnabled sql.NullBool\n\t\tsJWTGroupsClaimName sql.NullString\n\t\tsJWTAllowGroups sql.NullString\n\t\tsRoutingPeerDNSResolutionEnabled sql.NullBool\n\t\tsDNSDomain sql.NullString\n\t\tsNetworkRange sql.NullString\n\t\tsLazyConnectionEnabled sql.NullBool\n\t\tsExtraPeerApprovalEnabled sql.NullBool\n\t\tsExtraUserApprovalRequired sql.NullBool\n\t\tsExtraIntegratedValidator sql.NullString\n\t\tsExtraIntegratedValidatorGroups sql.NullString\n\t\tnetworkNet sql.NullString\n\t\tdnsSettingsDisabledGroups sql.NullString\n\t\tnetworkIdentifier sql.NullString\n\t\tnetworkDns sql.NullString\n\t\tnetworkSerial sql.NullInt64\n\t\tcreatedAt sql.NullTime\n\t)\n\terr := s.pool.QueryRow(ctx, accountQuery, accountID).Scan(\n\t\t&account.Id, &account.CreatedBy, &createdAt, &account.Domain, &account.DomainCategory, &account.IsDomainPrimaryAccount,\n\t\t&networkIdentifier, &networkNet, &networkDns, &networkSerial,\n\t\t&dnsSettingsDisabledGroups,\n\t\t&sPeerLoginExpirationEnabled, &sPeerLoginExpiration,\n\t\t&sPeerInactivityExpirationEnabled, &sPeerInactivityExpiration,\n\t\t&sRegularUsersViewBlocked, &sGroupsPropagationEnabled,\n\t\t&sJWTGroupsEnabled, &sJWTGroupsClaimName, &sJWTAllowGroups,\n\t\t&sRoutingPeerDNSResolutionEnabled, &sDNSDomain, &sNetworkRange,\n\t\t&sLazyConnectionEnabled,\n\t\t&sExtraPeerApprovalEnabled, &sExtraUserApprovalRequired,\n\t\t&sExtraIntegratedValidator, &sExtraIntegratedValidatorGroups,\n\t)\n\tif err != nil {\n\t\tif errors.Is(err, pgx.ErrNoRows) {\n\t\t\treturn nil, status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(err)\n\t}\n\n\taccount.Settings = &types.Settings{Extra: &types.ExtraSettings{}}\n\tif networkNet.Valid {\n\t\t_ = json.Unmarshal([]byte(networkNet.String), &account.Network.Net)\n\t}\n\tif createdAt.Valid {\n\t\taccount.CreatedAt = createdAt.Time\n\t}\n\tif dnsSettingsDisabledGroups.Valid {\n\t\t_ = json.Unmarshal([]byte(dnsSettingsDisabledGroups.String), &account.DNSSettings.DisabledManagementGroups)\n\t}\n\tif networkIdentifier.Valid {\n\t\taccount.Network.Identifier = networkIdentifier.String\n\t}\n\tif networkDns.Valid {\n\t\taccount.Network.Dns = networkDns.String\n\t}\n\tif networkSerial.Valid {\n\t\taccount.Network.Serial = uint64(networkSerial.Int64)\n\t}\n\tif sPeerLoginExpirationEnabled.Valid {\n\t\taccount.Settings.PeerLoginExpirationEnabled = sPeerLoginExpirationEnabled.Bool\n\t}\n\tif sPeerLoginExpiration.Valid {\n\t\taccount.Settings.PeerLoginExpiration = time.Duration(sPeerLoginExpiration.Int64)\n\t}\n\tif sPeerInactivityExpirationEnabled.Valid {\n\t\taccount.Settings.PeerInactivityExpirationEnabled = sPeerInactivityExpirationEnabled.Bool\n\t}\n\tif sPeerInactivityExpiration.Valid {\n\t\taccount.Settings.PeerInactivityExpiration = time.Duration(sPeerInactivityExpiration.Int64)\n\t}\n\tif sRegularUsersViewBlocked.Valid {\n\t\taccount.Settings.RegularUsersViewBlocked = sRegularUsersViewBlocked.Bool\n\t}\n\tif sGroupsPropagationEnabled.Valid {\n\t\taccount.Settings.GroupsPropagationEnabled = sGroupsPropagationEnabled.Bool\n\t}\n\tif sJWTGroupsEnabled.Valid {\n\t\taccount.Settings.JWTGroupsEnabled = sJWTGroupsEnabled.Bool\n\t}\n\tif sJWTGroupsClaimName.Valid {\n\t\taccount.Settings.JWTGroupsClaimName = sJWTGroupsClaimName.String\n\t}\n\tif sRoutingPeerDNSResolutionEnabled.Valid {\n\t\taccount.Settings.RoutingPeerDNSResolutionEnabled = sRoutingPeerDNSResolutionEnabled.Bool\n\t}\n\tif sDNSDomain.Valid {\n\t\taccount.Settings.DNSDomain = sDNSDomain.String\n\t}\n\tif sLazyConnectionEnabled.Valid {\n\t\taccount.Settings.LazyConnectionEnabled = sLazyConnectionEnabled.Bool\n\t}\n\tif sJWTAllowGroups.Valid {\n\t\t_ = json.Unmarshal([]byte(sJWTAllowGroups.String), &account.Settings.JWTAllowGroups)\n\t}\n\tif sNetworkRange.Valid {\n\t\t_ = json.Unmarshal([]byte(sNetworkRange.String), &account.Settings.NetworkRange)\n\t}\n\n\tif sExtraPeerApprovalEnabled.Valid {\n\t\taccount.Settings.Extra.PeerApprovalEnabled = sExtraPeerApprovalEnabled.Bool\n\t}\n\tif sExtraUserApprovalRequired.Valid {\n\t\taccount.Settings.Extra.UserApprovalRequired = sExtraUserApprovalRequired.Bool\n\t}\n\tif sExtraIntegratedValidator.Valid {\n\t\taccount.Settings.Extra.IntegratedValidator = sExtraIntegratedValidator.String\n\t}\n\tif sExtraIntegratedValidatorGroups.Valid {\n\t\t_ = json.Unmarshal([]byte(sExtraIntegratedValidatorGroups.String), &account.Settings.Extra.IntegratedValidatorGroups)\n\t}\n\taccount.InitOnce()\n\treturn &account, nil\n}\n\nfunc (s *SqlStore) getSetupKeys(ctx context.Context, accountID string) ([]types.SetupKey, error) {\n\tconst query = `SELECT id, account_id, key, key_secret, name, type, created_at, expires_at, updated_at, \n\trevoked, used_times, last_used, auto_groups, usage_limit, ephemeral, allow_extra_dns_labels FROM setup_keys WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tkeys, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (types.SetupKey, error) {\n\t\tvar sk types.SetupKey\n\t\tvar autoGroups []byte\n\t\tvar skCreatedAt, expiresAt, updatedAt, lastUsed sql.NullTime\n\t\tvar revoked, ephemeral, allowExtraDNSLabels sql.NullBool\n\t\tvar usedTimes, usageLimit sql.NullInt64\n\n\t\terr := row.Scan(&sk.Id, &sk.AccountID, &sk.Key, &sk.KeySecret, &sk.Name, &sk.Type, &skCreatedAt,\n\t\t\t&expiresAt, &updatedAt, &revoked, &usedTimes, &lastUsed, &autoGroups, &usageLimit, &ephemeral, &allowExtraDNSLabels)\n\n\t\tif err == nil {\n\t\t\tif expiresAt.Valid {\n\t\t\t\tsk.ExpiresAt = &expiresAt.Time\n\t\t\t}\n\t\t\tif skCreatedAt.Valid {\n\t\t\t\tsk.CreatedAt = skCreatedAt.Time\n\t\t\t}\n\t\t\tif updatedAt.Valid {\n\t\t\t\tsk.UpdatedAt = updatedAt.Time\n\t\t\t\tif sk.UpdatedAt.IsZero() {\n\t\t\t\t\tsk.UpdatedAt = sk.CreatedAt\n\t\t\t\t}\n\t\t\t}\n\t\t\tif lastUsed.Valid {\n\t\t\t\tsk.LastUsed = &lastUsed.Time\n\t\t\t}\n\t\t\tif revoked.Valid {\n\t\t\t\tsk.Revoked = revoked.Bool\n\t\t\t}\n\t\t\tif usedTimes.Valid {\n\t\t\t\tsk.UsedTimes = int(usedTimes.Int64)\n\t\t\t}\n\t\t\tif usageLimit.Valid {\n\t\t\t\tsk.UsageLimit = int(usageLimit.Int64)\n\t\t\t}\n\t\t\tif ephemeral.Valid {\n\t\t\t\tsk.Ephemeral = ephemeral.Bool\n\t\t\t}\n\t\t\tif allowExtraDNSLabels.Valid {\n\t\t\t\tsk.AllowExtraDNSLabels = allowExtraDNSLabels.Bool\n\t\t\t}\n\t\t\tif autoGroups != nil {\n\t\t\t\t_ = json.Unmarshal(autoGroups, &sk.AutoGroups)\n\t\t\t} else {\n\t\t\t\tsk.AutoGroups = []string{}\n\t\t\t}\n\t\t}\n\t\treturn sk, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn keys, nil\n}\n\nfunc (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Peer, error) {\n\tconst query = `SELECT id, account_id, key, ip, name, dns_label, user_id, ssh_key, ssh_enabled, login_expiration_enabled,\n\tinactivity_expiration_enabled, last_login, created_at, ephemeral, extra_dns_labels, allow_extra_dns_labels, meta_hostname, \n\tmeta_go_os, meta_kernel, meta_core, meta_platform, meta_os, meta_os_version, meta_wt_version, meta_ui_version, \n\tmeta_kernel_version, meta_network_addresses, meta_system_serial_number, meta_system_product_name, meta_system_manufacturer,\n\tmeta_environment, meta_flags, meta_files, peer_status_last_seen, peer_status_connected, peer_status_login_expired, \n\tpeer_status_requires_approval, location_connection_ip, location_country_code, location_city_name, \n\tlocation_geo_name_id, proxy_meta_embedded, proxy_meta_cluster FROM peers WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpeers, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (nbpeer.Peer, error) {\n\t\tvar p nbpeer.Peer\n\t\tp.Status = &nbpeer.PeerStatus{}\n\t\tvar (\n\t\t\tlastLogin, createdAt sql.NullTime\n\t\t\tsshEnabled, loginExpirationEnabled, inactivityExpirationEnabled, ephemeral, allowExtraDNSLabels sql.NullBool\n\t\t\tpeerStatusLastSeen sql.NullTime\n\t\t\tpeerStatusConnected, peerStatusLoginExpired, peerStatusRequiresApproval, proxyEmbedded sql.NullBool\n\t\t\tip, extraDNS, netAddr, env, flags, files, connIP []byte\n\t\t\tmetaHostname, metaGoOS, metaKernel, metaCore, metaPlatform sql.NullString\n\t\t\tmetaOS, metaOSVersion, metaWtVersion, metaUIVersion, metaKernelVersion sql.NullString\n\t\t\tmetaSystemSerialNumber, metaSystemProductName, metaSystemManufacturer sql.NullString\n\t\t\tlocationCountryCode, locationCityName, proxyCluster sql.NullString\n\t\t\tlocationGeoNameID sql.NullInt64\n\t\t)\n\n\t\terr := row.Scan(&p.ID, &p.AccountID, &p.Key, &ip, &p.Name, &p.DNSLabel, &p.UserID, &p.SSHKey, &sshEnabled,\n\t\t\t&loginExpirationEnabled, &inactivityExpirationEnabled, &lastLogin, &createdAt, &ephemeral, &extraDNS,\n\t\t\t&allowExtraDNSLabels, &metaHostname, &metaGoOS, &metaKernel, &metaCore, &metaPlatform,\n\t\t\t&metaOS, &metaOSVersion, &metaWtVersion, &metaUIVersion, &metaKernelVersion, &netAddr,\n\t\t\t&metaSystemSerialNumber, &metaSystemProductName, &metaSystemManufacturer, &env, &flags, &files,\n\t\t\t&peerStatusLastSeen, &peerStatusConnected, &peerStatusLoginExpired, &peerStatusRequiresApproval, &connIP,\n\t\t\t&locationCountryCode, &locationCityName, &locationGeoNameID, &proxyEmbedded, &proxyCluster)\n\n\t\tif err == nil {\n\t\t\tif lastLogin.Valid {\n\t\t\t\tp.LastLogin = &lastLogin.Time\n\t\t\t}\n\t\t\tif createdAt.Valid {\n\t\t\t\tp.CreatedAt = createdAt.Time\n\t\t\t}\n\t\t\tif sshEnabled.Valid {\n\t\t\t\tp.SSHEnabled = sshEnabled.Bool\n\t\t\t}\n\t\t\tif loginExpirationEnabled.Valid {\n\t\t\t\tp.LoginExpirationEnabled = loginExpirationEnabled.Bool\n\t\t\t}\n\t\t\tif inactivityExpirationEnabled.Valid {\n\t\t\t\tp.InactivityExpirationEnabled = inactivityExpirationEnabled.Bool\n\t\t\t}\n\t\t\tif ephemeral.Valid {\n\t\t\t\tp.Ephemeral = ephemeral.Bool\n\t\t\t}\n\t\t\tif allowExtraDNSLabels.Valid {\n\t\t\t\tp.AllowExtraDNSLabels = allowExtraDNSLabels.Bool\n\t\t\t}\n\t\t\tif peerStatusLastSeen.Valid {\n\t\t\t\tp.Status.LastSeen = peerStatusLastSeen.Time\n\t\t\t}\n\t\t\tif peerStatusConnected.Valid {\n\t\t\t\tp.Status.Connected = peerStatusConnected.Bool\n\t\t\t}\n\t\t\tif peerStatusLoginExpired.Valid {\n\t\t\t\tp.Status.LoginExpired = peerStatusLoginExpired.Bool\n\t\t\t}\n\t\t\tif peerStatusRequiresApproval.Valid {\n\t\t\t\tp.Status.RequiresApproval = peerStatusRequiresApproval.Bool\n\t\t\t}\n\t\t\tif metaHostname.Valid {\n\t\t\t\tp.Meta.Hostname = metaHostname.String\n\t\t\t}\n\t\t\tif metaGoOS.Valid {\n\t\t\t\tp.Meta.GoOS = metaGoOS.String\n\t\t\t}\n\t\t\tif metaKernel.Valid {\n\t\t\t\tp.Meta.Kernel = metaKernel.String\n\t\t\t}\n\t\t\tif metaCore.Valid {\n\t\t\t\tp.Meta.Core = metaCore.String\n\t\t\t}\n\t\t\tif metaPlatform.Valid {\n\t\t\t\tp.Meta.Platform = metaPlatform.String\n\t\t\t}\n\t\t\tif metaOS.Valid {\n\t\t\t\tp.Meta.OS = metaOS.String\n\t\t\t}\n\t\t\tif metaOSVersion.Valid {\n\t\t\t\tp.Meta.OSVersion = metaOSVersion.String\n\t\t\t}\n\t\t\tif metaWtVersion.Valid {\n\t\t\t\tp.Meta.WtVersion = metaWtVersion.String\n\t\t\t}\n\t\t\tif metaUIVersion.Valid {\n\t\t\t\tp.Meta.UIVersion = metaUIVersion.String\n\t\t\t}\n\t\t\tif metaKernelVersion.Valid {\n\t\t\t\tp.Meta.KernelVersion = metaKernelVersion.String\n\t\t\t}\n\t\t\tif metaSystemSerialNumber.Valid {\n\t\t\t\tp.Meta.SystemSerialNumber = metaSystemSerialNumber.String\n\t\t\t}\n\t\t\tif metaSystemProductName.Valid {\n\t\t\t\tp.Meta.SystemProductName = metaSystemProductName.String\n\t\t\t}\n\t\t\tif metaSystemManufacturer.Valid {\n\t\t\t\tp.Meta.SystemManufacturer = metaSystemManufacturer.String\n\t\t\t}\n\t\t\tif locationCountryCode.Valid {\n\t\t\t\tp.Location.CountryCode = locationCountryCode.String\n\t\t\t}\n\t\t\tif locationCityName.Valid {\n\t\t\t\tp.Location.CityName = locationCityName.String\n\t\t\t}\n\t\t\tif locationGeoNameID.Valid {\n\t\t\t\tp.Location.GeoNameID = uint(locationGeoNameID.Int64)\n\t\t\t}\n\t\t\tif proxyEmbedded.Valid {\n\t\t\t\tp.ProxyMeta.Embedded = proxyEmbedded.Bool\n\t\t\t}\n\t\t\tif proxyCluster.Valid {\n\t\t\t\tp.ProxyMeta.Cluster = proxyCluster.String\n\t\t\t}\n\t\t\tif ip != nil {\n\t\t\t\t_ = json.Unmarshal(ip, &p.IP)\n\t\t\t}\n\t\t\tif extraDNS != nil {\n\t\t\t\t_ = json.Unmarshal(extraDNS, &p.ExtraDNSLabels)\n\t\t\t}\n\t\t\tif netAddr != nil {\n\t\t\t\t_ = json.Unmarshal(netAddr, &p.Meta.NetworkAddresses)\n\t\t\t}\n\t\t\tif env != nil {\n\t\t\t\t_ = json.Unmarshal(env, &p.Meta.Environment)\n\t\t\t}\n\t\t\tif flags != nil {\n\t\t\t\t_ = json.Unmarshal(flags, &p.Meta.Flags)\n\t\t\t}\n\t\t\tif files != nil {\n\t\t\t\t_ = json.Unmarshal(files, &p.Meta.Files)\n\t\t\t}\n\t\t\tif connIP != nil {\n\t\t\t\t_ = json.Unmarshal(connIP, &p.Location.ConnectionIP)\n\t\t\t}\n\t\t}\n\t\treturn p, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn peers, nil\n}\n\nfunc (s *SqlStore) getUsers(ctx context.Context, accountID string) ([]types.User, error) {\n\tconst query = `SELECT id, account_id, role, is_service_user, non_deletable, service_user_name, auto_groups, blocked, pending_approval, last_login, created_at, issued, integration_ref_id, integration_ref_integration_type, email, name FROM users WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tusers, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (types.User, error) {\n\t\tvar u types.User\n\t\tvar autoGroups []byte\n\t\tvar lastLogin, createdAt sql.NullTime\n\t\tvar isServiceUser, nonDeletable, blocked, pendingApproval sql.NullBool\n\t\terr := row.Scan(&u.Id, &u.AccountID, &u.Role, &isServiceUser, &nonDeletable, &u.ServiceUserName, &autoGroups, &blocked, &pendingApproval, &lastLogin, &createdAt, &u.Issued, &u.IntegrationReference.ID, &u.IntegrationReference.IntegrationType, &u.Email, &u.Name)\n\t\tif err == nil {\n\t\t\tif lastLogin.Valid {\n\t\t\t\tu.LastLogin = &lastLogin.Time\n\t\t\t}\n\t\t\tif createdAt.Valid {\n\t\t\t\tu.CreatedAt = createdAt.Time\n\t\t\t}\n\t\t\tif isServiceUser.Valid {\n\t\t\t\tu.IsServiceUser = isServiceUser.Bool\n\t\t\t}\n\t\t\tif nonDeletable.Valid {\n\t\t\t\tu.NonDeletable = nonDeletable.Bool\n\t\t\t}\n\t\t\tif blocked.Valid {\n\t\t\t\tu.Blocked = blocked.Bool\n\t\t\t}\n\t\t\tif pendingApproval.Valid {\n\t\t\t\tu.PendingApproval = pendingApproval.Bool\n\t\t\t}\n\t\t\tif autoGroups != nil {\n\t\t\t\t_ = json.Unmarshal(autoGroups, &u.AutoGroups)\n\t\t\t} else {\n\t\t\t\tu.AutoGroups = []string{}\n\t\t\t}\n\t\t}\n\t\treturn u, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn users, nil\n}\n\nfunc (s *SqlStore) getGroups(ctx context.Context, accountID string) ([]*types.Group, error) {\n\tconst query = `SELECT id, account_id, name, issued, resources, integration_ref_id, integration_ref_integration_type FROM groups WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroups, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (*types.Group, error) {\n\t\tvar g types.Group\n\t\tvar resources []byte\n\t\tvar refID sql.NullInt64\n\t\tvar refType sql.NullString\n\t\terr := row.Scan(&g.ID, &g.AccountID, &g.Name, &g.Issued, &resources, &refID, &refType)\n\t\tif err == nil {\n\t\t\tif refID.Valid {\n\t\t\t\tg.IntegrationReference.ID = int(refID.Int64)\n\t\t\t}\n\t\t\tif refType.Valid {\n\t\t\t\tg.IntegrationReference.IntegrationType = refType.String\n\t\t\t}\n\t\t\tif resources != nil {\n\t\t\t\t_ = json.Unmarshal(resources, &g.Resources)\n\t\t\t} else {\n\t\t\t\tg.Resources = []types.Resource{}\n\t\t\t}\n\t\t\tg.GroupPeers = []types.GroupPeer{}\n\t\t\tg.Peers = []string{}\n\t\t}\n\t\treturn &g, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn groups, nil\n}\n\nfunc (s *SqlStore) getPolicies(ctx context.Context, accountID string) ([]*types.Policy, error) {\n\tconst query = `SELECT id, account_id, name, description, enabled, source_posture_checks FROM policies WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpolicies, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (*types.Policy, error) {\n\t\tvar p types.Policy\n\t\tvar checks []byte\n\t\tvar enabled sql.NullBool\n\t\terr := row.Scan(&p.ID, &p.AccountID, &p.Name, &p.Description, &enabled, &checks)\n\t\tif err == nil {\n\t\t\tif enabled.Valid {\n\t\t\t\tp.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif checks != nil {\n\t\t\t\t_ = json.Unmarshal(checks, &p.SourcePostureChecks)\n\t\t\t}\n\t\t}\n\t\treturn &p, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn policies, nil\n}\n\nfunc (s *SqlStore) getRoutes(ctx context.Context, accountID string) ([]route.Route, error) {\n\tconst query = `SELECT id, account_id, network, domains, keep_route, net_id, description, peer, peer_groups, network_type, masquerade, metric, enabled, groups, access_control_groups, skip_auto_apply FROM routes WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\troutes, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (route.Route, error) {\n\t\tvar r route.Route\n\t\tvar network, domains, peerGroups, groups, accessGroups []byte\n\t\tvar keepRoute, masquerade, enabled, skipAutoApply sql.NullBool\n\t\tvar metric sql.NullInt64\n\t\terr := row.Scan(&r.ID, &r.AccountID, &network, &domains, &keepRoute, &r.NetID, &r.Description, &r.Peer, &peerGroups, &r.NetworkType, &masquerade, &metric, &enabled, &groups, &accessGroups, &skipAutoApply)\n\t\tif err == nil {\n\t\t\tif keepRoute.Valid {\n\t\t\t\tr.KeepRoute = keepRoute.Bool\n\t\t\t}\n\t\t\tif masquerade.Valid {\n\t\t\t\tr.Masquerade = masquerade.Bool\n\t\t\t}\n\t\t\tif enabled.Valid {\n\t\t\t\tr.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif skipAutoApply.Valid {\n\t\t\t\tr.SkipAutoApply = skipAutoApply.Bool\n\t\t\t}\n\t\t\tif metric.Valid {\n\t\t\t\tr.Metric = int(metric.Int64)\n\t\t\t}\n\t\t\tif network != nil {\n\t\t\t\t_ = json.Unmarshal(network, &r.Network)\n\t\t\t}\n\t\t\tif domains != nil {\n\t\t\t\t_ = json.Unmarshal(domains, &r.Domains)\n\t\t\t}\n\t\t\tif peerGroups != nil {\n\t\t\t\t_ = json.Unmarshal(peerGroups, &r.PeerGroups)\n\t\t\t}\n\t\t\tif groups != nil {\n\t\t\t\t_ = json.Unmarshal(groups, &r.Groups)\n\t\t\t}\n\t\t\tif accessGroups != nil {\n\t\t\t\t_ = json.Unmarshal(accessGroups, &r.AccessControlGroups)\n\t\t\t}\n\t\t}\n\t\treturn r, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn routes, nil\n}\n\nfunc (s *SqlStore) getNameServerGroups(ctx context.Context, accountID string) ([]nbdns.NameServerGroup, error) {\n\tconst query = `SELECT id, account_id, name, description, name_servers, groups, \"primary\", domains, enabled, search_domains_enabled FROM name_server_groups WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tnsgs, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (nbdns.NameServerGroup, error) {\n\t\tvar n nbdns.NameServerGroup\n\t\tvar ns, groups, domains []byte\n\t\tvar primary, enabled, searchDomainsEnabled sql.NullBool\n\t\terr := row.Scan(&n.ID, &n.AccountID, &n.Name, &n.Description, &ns, &groups, &primary, &domains, &enabled, &searchDomainsEnabled)\n\t\tif err == nil {\n\t\t\tif primary.Valid {\n\t\t\t\tn.Primary = primary.Bool\n\t\t\t}\n\t\t\tif enabled.Valid {\n\t\t\t\tn.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif searchDomainsEnabled.Valid {\n\t\t\t\tn.SearchDomainsEnabled = searchDomainsEnabled.Bool\n\t\t\t}\n\t\t\tif ns != nil {\n\t\t\t\t_ = json.Unmarshal(ns, &n.NameServers)\n\t\t\t} else {\n\t\t\t\tn.NameServers = []nbdns.NameServer{}\n\t\t\t}\n\t\t\tif groups != nil {\n\t\t\t\t_ = json.Unmarshal(groups, &n.Groups)\n\t\t\t} else {\n\t\t\t\tn.Groups = []string{}\n\t\t\t}\n\t\t\tif domains != nil {\n\t\t\t\t_ = json.Unmarshal(domains, &n.Domains)\n\t\t\t} else {\n\t\t\t\tn.Domains = []string{}\n\t\t\t}\n\t\t}\n\t\treturn n, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn nsgs, nil\n}\n\nfunc (s *SqlStore) getPostureChecks(ctx context.Context, accountID string) ([]*posture.Checks, error) {\n\tconst query = `SELECT id, account_id, name, description, checks FROM posture_checks WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tchecks, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (*posture.Checks, error) {\n\t\tvar c posture.Checks\n\t\tvar checksDef []byte\n\t\terr := row.Scan(&c.ID, &c.AccountID, &c.Name, &c.Description, &checksDef)\n\t\tif err == nil && checksDef != nil {\n\t\t\t_ = json.Unmarshal(checksDef, &c.Checks)\n\t\t}\n\t\treturn &c, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn checks, nil\n}\n\nfunc (s *SqlStore) getServices(ctx context.Context, accountID string) ([]*reverseproxy.Service, error) {\n\tconst serviceQuery = `SELECT id, account_id, name, domain, enabled, auth,\n\t\tmeta_created_at, meta_certificate_issued_at, meta_status, proxy_cluster,\n\t\tpass_host_header, rewrite_redirects, session_private_key, session_public_key\n\t\tFROM services WHERE account_id = $1`\n\n\tconst targetsQuery = `SELECT id, account_id, service_id, path, host, port, protocol,\n\t\ttarget_id, target_type, enabled\n\t\tFROM targets WHERE service_id = ANY($1)`\n\n\tserviceRows, err := s.pool.Query(ctx, serviceQuery, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tservices, err := pgx.CollectRows(serviceRows, func(row pgx.CollectableRow) (*reverseproxy.Service, error) {\n\t\tvar s reverseproxy.Service\n\t\tvar auth []byte\n\t\tvar createdAt, certIssuedAt sql.NullTime\n\t\tvar status, proxyCluster, sessionPrivateKey, sessionPublicKey sql.NullString\n\t\terr := row.Scan(\n\t\t\t&s.ID,\n\t\t\t&s.AccountID,\n\t\t\t&s.Name,\n\t\t\t&s.Domain,\n\t\t\t&s.Enabled,\n\t\t\t&auth,\n\t\t\t&createdAt,\n\t\t\t&certIssuedAt,\n\t\t\t&status,\n\t\t\t&proxyCluster,\n\t\t\t&s.PassHostHeader,\n\t\t\t&s.RewriteRedirects,\n\t\t\t&sessionPrivateKey,\n\t\t\t&sessionPublicKey,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif auth != nil {\n\t\t\tif err := json.Unmarshal(auth, &s.Auth); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\ts.Meta = reverseproxy.ServiceMeta{}\n\t\tif createdAt.Valid {\n\t\t\ts.Meta.CreatedAt = createdAt.Time\n\t\t}\n\t\tif certIssuedAt.Valid {\n\t\t\tt := certIssuedAt.Time\n\t\t\ts.Meta.CertificateIssuedAt = &t\n\t\t}\n\t\tif status.Valid {\n\t\t\ts.Meta.Status = status.String\n\t\t}\n\t\tif proxyCluster.Valid {\n\t\t\ts.ProxyCluster = proxyCluster.String\n\t\t}\n\t\tif sessionPrivateKey.Valid {\n\t\t\ts.SessionPrivateKey = sessionPrivateKey.String\n\t\t}\n\t\tif sessionPublicKey.Valid {\n\t\t\ts.SessionPublicKey = sessionPublicKey.String\n\t\t}\n\n\t\ts.Targets = []*reverseproxy.Target{}\n\t\treturn &s, nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(services) == 0 {\n\t\treturn services, nil\n\t}\n\n\tserviceIDs := make([]string, len(services))\n\tserviceMap := make(map[string]*reverseproxy.Service)\n\tfor i, s := range services {\n\t\tserviceIDs[i] = s.ID\n\t\tserviceMap[s.ID] = s\n\t}\n\n\ttargetRows, err := s.pool.Query(ctx, targetsQuery, serviceIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttargets, err := pgx.CollectRows(targetRows, func(row pgx.CollectableRow) (*reverseproxy.Target, error) {\n\t\tvar t reverseproxy.Target\n\t\tvar path sql.NullString\n\t\terr := row.Scan(\n\t\t\t&t.ID,\n\t\t\t&t.AccountID,\n\t\t\t&t.ServiceID,\n\t\t\t&path,\n\t\t\t&t.Host,\n\t\t\t&t.Port,\n\t\t\t&t.Protocol,\n\t\t\t&t.TargetId,\n\t\t\t&t.TargetType,\n\t\t\t&t.Enabled,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif path.Valid {\n\t\t\tt.Path = &path.String\n\t\t}\n\t\treturn &t, nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, target := range targets {\n\t\tif service, ok := serviceMap[target.ServiceID]; ok {\n\t\t\tservice.Targets = append(service.Targets, target)\n\t\t}\n\t}\n\n\treturn services, nil\n}\n\nfunc (s *SqlStore) getNetworks(ctx context.Context, accountID string) ([]*networkTypes.Network, error) {\n\tconst query = `SELECT id, account_id, name, description FROM networks WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tnetworks, err := pgx.CollectRows(rows, pgx.RowToStructByName[networkTypes.Network])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult := make([]*networkTypes.Network, len(networks))\n\tfor i := range networks {\n\t\tresult[i] = &networks[i]\n\t}\n\treturn result, nil\n}\n\nfunc (s *SqlStore) getNetworkRouters(ctx context.Context, accountID string) ([]*routerTypes.NetworkRouter, error) {\n\tconst query = `SELECT id, network_id, account_id, peer, peer_groups, masquerade, metric, enabled FROM network_routers WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trouters, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (routerTypes.NetworkRouter, error) {\n\t\tvar r routerTypes.NetworkRouter\n\t\tvar peerGroups []byte\n\t\tvar masquerade, enabled sql.NullBool\n\t\tvar metric sql.NullInt64\n\t\terr := row.Scan(&r.ID, &r.NetworkID, &r.AccountID, &r.Peer, &peerGroups, &masquerade, &metric, &enabled)\n\t\tif err == nil {\n\t\t\tif masquerade.Valid {\n\t\t\t\tr.Masquerade = masquerade.Bool\n\t\t\t}\n\t\t\tif enabled.Valid {\n\t\t\t\tr.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif metric.Valid {\n\t\t\t\tr.Metric = int(metric.Int64)\n\t\t\t}\n\t\t\tif peerGroups != nil {\n\t\t\t\t_ = json.Unmarshal(peerGroups, &r.PeerGroups)\n\t\t\t}\n\t\t}\n\t\treturn r, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult := make([]*routerTypes.NetworkRouter, len(routers))\n\tfor i := range routers {\n\t\tresult[i] = &routers[i]\n\t}\n\treturn result, nil\n}\n\nfunc (s *SqlStore) getNetworkResources(ctx context.Context, accountID string) ([]*resourceTypes.NetworkResource, error) {\n\tconst query = `SELECT id, network_id, account_id, name, description, type, domain, prefix, enabled FROM network_resources WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresources, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (resourceTypes.NetworkResource, error) {\n\t\tvar r resourceTypes.NetworkResource\n\t\tvar prefix []byte\n\t\tvar enabled sql.NullBool\n\t\terr := row.Scan(&r.ID, &r.NetworkID, &r.AccountID, &r.Name, &r.Description, &r.Type, &r.Domain, &prefix, &enabled)\n\t\tif err == nil {\n\t\t\tif enabled.Valid {\n\t\t\t\tr.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif prefix != nil {\n\t\t\t\t_ = json.Unmarshal(prefix, &r.Prefix)\n\t\t\t}\n\t\t}\n\t\treturn r, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult := make([]*resourceTypes.NetworkResource, len(resources))\n\tfor i := range resources {\n\t\tresult[i] = &resources[i]\n\t}\n\treturn result, nil\n}\n\nfunc (s *SqlStore) getAccountOnboarding(ctx context.Context, accountID string, account *types.Account) error {\n\tconst query = `SELECT account_id, onboarding_flow_pending, signup_form_pending, created_at, updated_at FROM account_onboardings WHERE account_id = $1`\n\tvar onboardingFlowPending, signupFormPending sql.NullBool\n\tvar createdAt, updatedAt sql.NullTime\n\terr := s.pool.QueryRow(ctx, query, accountID).Scan(\n\t\t&account.Onboarding.AccountID,\n\t\t&onboardingFlowPending,\n\t\t&signupFormPending,\n\t\t&createdAt,\n\t\t&updatedAt,\n\t)\n\tif err != nil && !errors.Is(err, pgx.ErrNoRows) {\n\t\treturn err\n\t}\n\tif createdAt.Valid {\n\t\taccount.Onboarding.CreatedAt = createdAt.Time\n\t}\n\tif updatedAt.Valid {\n\t\taccount.Onboarding.UpdatedAt = updatedAt.Time\n\t}\n\tif onboardingFlowPending.Valid {\n\t\taccount.Onboarding.OnboardingFlowPending = onboardingFlowPending.Bool\n\t}\n\tif signupFormPending.Valid {\n\t\taccount.Onboarding.SignupFormPending = signupFormPending.Bool\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) getPersonalAccessTokens(ctx context.Context, userIDs []string) ([]types.PersonalAccessToken, error) {\n\tif len(userIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\tconst query = `SELECT id, user_id, name, hashed_token, expiration_date, created_by, created_at, last_used FROM personal_access_tokens WHERE user_id = ANY($1)`\n\trows, err := s.pool.Query(ctx, query, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpats, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (types.PersonalAccessToken, error) {\n\t\tvar pat types.PersonalAccessToken\n\t\tvar expirationDate, lastUsed, createdAt sql.NullTime\n\t\terr := row.Scan(&pat.ID, &pat.UserID, &pat.Name, &pat.HashedToken, &expirationDate, &pat.CreatedBy, &createdAt, &lastUsed)\n\t\tif err == nil {\n\t\t\tif expirationDate.Valid {\n\t\t\t\tpat.ExpirationDate = &expirationDate.Time\n\t\t\t}\n\t\t\tif createdAt.Valid {\n\t\t\t\tpat.CreatedAt = createdAt.Time\n\t\t\t}\n\t\t\tif lastUsed.Valid {\n\t\t\t\tpat.LastUsed = &lastUsed.Time\n\t\t\t}\n\t\t}\n\t\treturn pat, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn pats, nil\n}\n\nfunc (s *SqlStore) getPolicyRules(ctx context.Context, policyIDs []string) ([]*types.PolicyRule, error) {\n\tif len(policyIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\tconst query = `SELECT id, policy_id, name, description, enabled, action, destinations, destination_resource, sources, source_resource, bidirectional, protocol, ports, port_ranges, authorized_groups, authorized_user FROM policy_rules WHERE policy_id = ANY($1)`\n\trows, err := s.pool.Query(ctx, query, policyIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trules, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (*types.PolicyRule, error) {\n\t\tvar r types.PolicyRule\n\t\tvar dest, destRes, sources, sourceRes, ports, portRanges, authorizedGroups []byte\n\t\tvar enabled, bidirectional sql.NullBool\n\t\tvar authorizedUser sql.NullString\n\t\terr := row.Scan(&r.ID, &r.PolicyID, &r.Name, &r.Description, &enabled, &r.Action, &dest, &destRes, &sources, &sourceRes, &bidirectional, &r.Protocol, &ports, &portRanges, &authorizedGroups, &authorizedUser)\n\t\tif err == nil {\n\t\t\tif enabled.Valid {\n\t\t\t\tr.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif bidirectional.Valid {\n\t\t\t\tr.Bidirectional = bidirectional.Bool\n\t\t\t}\n\t\t\tif dest != nil {\n\t\t\t\t_ = json.Unmarshal(dest, &r.Destinations)\n\t\t\t}\n\t\t\tif destRes != nil {\n\t\t\t\t_ = json.Unmarshal(destRes, &r.DestinationResource)\n\t\t\t}\n\t\t\tif sources != nil {\n\t\t\t\t_ = json.Unmarshal(sources, &r.Sources)\n\t\t\t}\n\t\t\tif sourceRes != nil {\n\t\t\t\t_ = json.Unmarshal(sourceRes, &r.SourceResource)\n\t\t\t}\n\t\t\tif ports != nil {\n\t\t\t\t_ = json.Unmarshal(ports, &r.Ports)\n\t\t\t}\n\t\t\tif portRanges != nil {\n\t\t\t\t_ = json.Unmarshal(portRanges, &r.PortRanges)\n\t\t\t}\n\t\t\tif authorizedGroups != nil {\n\t\t\t\t_ = json.Unmarshal(authorizedGroups, &r.AuthorizedGroups)\n\t\t\t}\n\t\t\tif authorizedUser.Valid {\n\t\t\t\tr.AuthorizedUser = authorizedUser.String\n\t\t\t}\n\t\t}\n\t\treturn &r, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn rules, nil\n}\n\nfunc (s *SqlStore) getGroupPeers(ctx context.Context, groupIDs []string) ([]types.GroupPeer, error) {\n\tif len(groupIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\tconst query = `SELECT account_id, group_id, peer_id FROM group_peers WHERE group_id = ANY($1)`\n\trows, err := s.pool.Query(ctx, query, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroupPeers, err := pgx.CollectRows(rows, pgx.RowToStructByName[types.GroupPeer])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn groupPeers, nil\n}\n\nfunc (s *SqlStore) GetAccountByUser(ctx context.Context, userID string) (*types.Account, error) {\n\tvar user types.User\n\tresult := s.db.Select(\"account_id\").Take(&user, idQueryCondition, userID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\tif user.AccountID == \"\" {\n\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn s.GetAccount(ctx, user.AccountID)\n}\n\nfunc (s *SqlStore) GetAccountByPeerID(ctx context.Context, peerID string) (*types.Account, error) {\n\tvar peer nbpeer.Peer\n\tresult := s.db.Select(\"account_id\").Take(&peer, idQueryCondition, peerID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\tif peer.AccountID == \"\" {\n\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn s.GetAccount(ctx, peer.AccountID)\n}\n\nfunc (s *SqlStore) GetAccountByPeerPubKey(ctx context.Context, peerKey string) (*types.Account, error) {\n\tvar peer nbpeer.Peer\n\tresult := s.db.Select(\"account_id\").Take(&peer, GetKeyQueryCondition(s), peerKey)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\tif peer.AccountID == \"\" {\n\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn s.GetAccount(ctx, peer.AccountID)\n}\n\nfunc (s *SqlStore) GetAnyAccountID(ctx context.Context) (string, error) {\n\tvar account types.Account\n\tresult := s.db.Select(\"id\").Order(\"created_at desc\").Limit(1).Find(&account)\n\tif result.Error != nil {\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\tif result.RowsAffected == 0 {\n\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn account.Id, nil\n}\n\nfunc (s *SqlStore) GetAccountIDByPeerPubKey(ctx context.Context, peerKey string) (string, error) {\n\tvar peer nbpeer.Peer\n\tvar accountID string\n\tresult := s.db.Model(&peer).Select(\"account_id\").Where(GetKeyQueryCondition(s), peerKey).Take(&accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn accountID, nil\n}\n\nfunc (s *SqlStore) GetAccountIDByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountID string\n\tresult := tx.Model(&types.User{}).\n\t\tSelect(\"account_id\").Where(idQueryCondition, userID).Take(&accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn accountID, nil\n}\n\nfunc (s *SqlStore) GetAccountIDByPeerID(ctx context.Context, lockStrength LockingStrength, peerID string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountID string\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tSelect(\"account_id\").Where(idQueryCondition, peerID).Take(&accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"peer %s account not found\", peerID)\n\t\t}\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn accountID, nil\n}\n\nfunc (s *SqlStore) GetAccountIDBySetupKey(ctx context.Context, setupKey string) (string, error) {\n\tvar accountID string\n\tresult := s.db.Model(&types.SetupKey{}).Select(\"account_id\").Where(GetKeyQueryCondition(s), setupKey).Take(&accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.NewSetupKeyNotFoundError(setupKey)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get account ID by setup key from store: %v\", result.Error)\n\t\treturn \"\", status.Errorf(status.Internal, \"failed to get account ID by setup key from store\")\n\t}\n\n\tif accountID == \"\" {\n\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn accountID, nil\n}\n\nfunc (s *SqlStore) GetTakenIPs(ctx context.Context, lockStrength LockingStrength, accountID string) ([]net.IP, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar ipJSONStrings []string\n\n\t// Fetch the IP addresses as JSON strings\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tWhere(\"account_id = ?\", accountID).\n\t\tPluck(\"ip\", &ipJSONStrings)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"no peers found for the account\")\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting IPs from store: %s\", result.Error)\n\t}\n\n\t// Convert the JSON strings to net.IP objects\n\tips := make([]net.IP, len(ipJSONStrings))\n\tfor i, ipJSON := range ipJSONStrings {\n\t\tvar ip net.IP\n\t\tif err := json.Unmarshal([]byte(ipJSON), &ip); err != nil {\n\t\t\treturn nil, status.Errorf(status.Internal, \"issue parsing IP JSON from store\")\n\t\t}\n\t\tips[i] = ip\n\t}\n\n\treturn ips, nil\n}\n\nfunc (s *SqlStore) GetPeerLabelsInAccount(ctx context.Context, lockStrength LockingStrength, accountID string, dnsLabel string) ([]string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar labels []string\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tWhere(\"account_id = ? AND dns_label LIKE ?\", accountID, dnsLabel+\"%\").\n\t\tPluck(\"dns_label\", &labels)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"no peers found for the account\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"error when getting dns labels from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting dns labels from store: %s\", result.Error)\n\t}\n\n\treturn labels, nil\n}\n\nfunc (s *SqlStore) GetAccountNetwork(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.Network, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountNetwork types.AccountNetwork\n\tif err := tx.Model(&types.Account{}).Where(idQueryCondition, accountID).Take(&accountNetwork).Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting network from store: %s\", err)\n\t}\n\treturn accountNetwork.Network, nil\n}\n\nfunc (s *SqlStore) GetPeerByPeerPubKey(ctx context.Context, lockStrength LockingStrength, peerKey string) (*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peer nbpeer.Peer\n\tresult := tx.Take(&peer, GetKeyQueryCondition(s), peerKey)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPeerNotFoundError(peerKey)\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting peer from store: %s\", result.Error)\n\t}\n\n\treturn &peer, nil\n}\n\nfunc (s *SqlStore) GetAccountSettings(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.Settings, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountSettings types.AccountSettings\n\tif err := tx.Model(&types.Account{}).Where(idQueryCondition, accountID).Take(&accountSettings).Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"settings not found\")\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting settings from store: %s\", err)\n\t}\n\treturn accountSettings.Settings, nil\n}\n\nfunc (s *SqlStore) GetAccountCreatedBy(ctx context.Context, lockStrength LockingStrength, accountID string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar createdBy string\n\tresult := tx.Model(&types.Account{}).\n\t\tSelect(\"created_by\").Take(&createdBy, idQueryCondition, accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn createdBy, nil\n}\n\n// SaveUserLastLogin stores the last login time for a user in DB.\nfunc (s *SqlStore) SaveUserLastLogin(ctx context.Context, accountID, userID string, lastLogin time.Time) error {\n\tvar user types.User\n\tresult := s.db.Take(&user, accountAndIDQueryCondition, accountID, userID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn status.NewUserNotFoundError(userID)\n\t\t}\n\t\treturn status.NewGetUserFromStoreError()\n\t}\n\n\tif !lastLogin.IsZero() {\n\t\tuser.LastLogin = &lastLogin\n\t\treturn s.db.Save(&user).Error\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetPostureCheckByChecksDefinition(accountID string, checks *posture.ChecksDefinition) (*posture.Checks, error) {\n\tdefinitionJSON, err := json.Marshal(checks)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar postureCheck posture.Checks\n\terr = s.db.Where(\"account_id = ? AND checks = ?\", accountID, string(definitionJSON)).Take(&postureCheck).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &postureCheck, nil\n}\n\n// Close closes the underlying DB connection\nfunc (s *SqlStore) Close(_ context.Context) error {\n\tsql, err := s.db.DB()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get db: %w\", err)\n\t}\n\treturn sql.Close()\n}\n\n// GetStoreEngine returns underlying store engine\nfunc (s *SqlStore) GetStoreEngine() types.Engine {\n\treturn s.storeEngine\n}\n\n// NewSqliteStore creates a new SQLite store.\nfunc NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tstoreFile := storeSqliteFileName\n\tif envFile, ok := os.LookupEnv(\"NB_STORE_ENGINE_SQLITE_FILE\"); ok && envFile != \"\" {\n\t\tstoreFile = envFile\n\t}\n\n\t// Separate file path from any SQLite URI query parameters (e.g., \"store.db?mode=rwc\")\n\tfilePath, query, hasQuery := strings.Cut(storeFile, \"?\")\n\n\tconnStr := filePath\n\tif !filepath.IsAbs(filePath) {\n\t\tconnStr = filepath.Join(dataDir, filePath)\n\t}\n\n\t// Append query parameters: user-provided take precedence, otherwise default to cache=shared on non-Windows\n\tif hasQuery {\n\t\tconnStr += \"?\" + query\n\t} else if runtime.GOOS != \"windows\" {\n\t\t// To avoid `The process cannot access the file because it is being used by another process` on Windows\n\t\tconnStr += \"?cache=shared\"\n\t}\n\n\tdb, err := gorm.Open(sqlite.Open(connStr), getGormConfig())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn NewSqlStore(ctx, db, types.SqliteStoreEngine, metrics, skipMigration)\n}\n\n// NewPostgresqlStore creates a new Postgres store.\nfunc NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tdb, err := gorm.Open(postgres.Open(dsn), getGormConfig())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpool, err := connectToPgDb(context.Background(), dsn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tstore, err := NewSqlStore(ctx, db, types.PostgresStoreEngine, metrics, skipMigration)\n\tif err != nil {\n\t\tpool.Close()\n\t\treturn nil, err\n\t}\n\tstore.pool = pool\n\treturn store, nil\n}\n\nfunc connectToPgDb(ctx context.Context, dsn string) (*pgxpool.Pool, error) {\n\tconfig, err := pgxpool.ParseConfig(dsn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse database config: %w\", err)\n\t}\n\n\tconfig.MaxConns = pgMaxConnections\n\tconfig.MinConns = pgMinConnections\n\tconfig.MaxConnLifetime = pgMaxConnLifetime\n\tconfig.HealthCheckPeriod = pgHealthCheckPeriod\n\n\tpool, err := pgxpool.NewWithConfig(ctx, config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create connection pool: %w\", err)\n\t}\n\n\tif err := pool.Ping(ctx); err != nil {\n\t\tpool.Close()\n\t\treturn nil, fmt.Errorf(\"unable to ping database: %w\", err)\n\t}\n\n\treturn pool, nil\n}\n\n// NewMysqlStore creates a new MySQL store.\nfunc NewMysqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tdb, err := gorm.Open(mysql.Open(dsn+\"?charset=utf8&parseTime=True&loc=Local\"), getGormConfig())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn NewSqlStore(ctx, db, types.MysqlStoreEngine, metrics, skipMigration)\n}\n\nfunc getGormConfig() *gorm.Config {\n\treturn &gorm.Config{\n\t\tLogger: logger.Default.LogMode(logger.Silent),\n\t\tCreateBatchSize: 400,\n\t}\n}\n\n// newPostgresStore initializes a new Postgres store.\nfunc newPostgresStore(ctx context.Context, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {\n\tdsn, ok := lookupDSNEnv(postgresDsnEnv, postgresDsnEnvLegacy)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"%s is not set\", postgresDsnEnv)\n\t}\n\treturn NewPostgresqlStore(ctx, dsn, metrics, skipMigration)\n}\n\n// newMysqlStore initializes a new MySQL store.\nfunc newMysqlStore(ctx context.Context, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {\n\tdsn, ok := lookupDSNEnv(mysqlDsnEnv, mysqlDsnEnvLegacy)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"%s is not set\", mysqlDsnEnv)\n\t}\n\treturn NewMysqlStore(ctx, dsn, metrics, skipMigration)\n}\n\n// NewSqliteStoreFromFileStore restores a store from FileStore and stores SQLite DB in the file located in datadir.\nfunc NewSqliteStoreFromFileStore(ctx context.Context, fileStore *FileStore, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tstore, err := NewSqliteStore(ctx, dataDir, metrics, skipMigration)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = store.SaveInstallationID(ctx, fileStore.InstallationID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, account := range fileStore.GetAllAccounts(ctx) {\n\t\t_, err = account.GetGroupAll()\n\t\tif err != nil {\n\t\t\tif err := account.AddAllGroup(false); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\terr := store.SaveAccount(ctx, account)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn store, nil\n}\n\n// NewPostgresqlStoreFromSqlStore restores a store from SqlStore and stores Postgres DB.\nfunc NewPostgresqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) {\n\tstore, err := NewPostgresqlStoreForTests(ctx, dsn, metrics, false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = store.SaveInstallationID(ctx, sqliteStore.GetInstallationID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, account := range sqliteStore.GetAllAccounts(ctx) {\n\t\terr := store.SaveAccount(ctx, account)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn store, nil\n}\n\n// used for tests only\nfunc NewPostgresqlStoreForTests(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tdb, err := gorm.Open(postgres.Open(dsn), getGormConfig())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpool, err := connectToPgDbForTests(context.Background(), dsn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tstore, err := NewSqlStore(ctx, db, types.PostgresStoreEngine, metrics, skipMigration)\n\tif err != nil {\n\t\tpool.Close()\n\t\treturn nil, err\n\t}\n\tstore.pool = pool\n\treturn store, nil\n}\n\n// used for tests only\nfunc connectToPgDbForTests(ctx context.Context, dsn string) (*pgxpool.Pool, error) {\n\tconfig, err := pgxpool.ParseConfig(dsn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse database config: %w\", err)\n\t}\n\n\tconfig.MaxConns = 5\n\tconfig.MinConns = 1\n\tconfig.MaxConnLifetime = 30 * time.Second\n\tconfig.HealthCheckPeriod = 10 * time.Second\n\n\tpool, err := pgxpool.NewWithConfig(ctx, config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create connection pool: %w\", err)\n\t}\n\n\tif err := pool.Ping(ctx); err != nil {\n\t\tpool.Close()\n\t\treturn nil, fmt.Errorf(\"unable to ping database: %w\", err)\n\t}\n\n\treturn pool, nil\n}\n\n// NewMysqlStoreFromSqlStore restores a store from SqlStore and stores MySQL DB.\nfunc NewMysqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) {\n\tstore, err := NewMysqlStore(ctx, dsn, metrics, false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = store.SaveInstallationID(ctx, sqliteStore.GetInstallationID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, account := range sqliteStore.GetAllAccounts(ctx) {\n\t\terr := store.SaveAccount(ctx, account)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn store, nil\n}\n\nfunc (s *SqlStore) GetSetupKeyBySecret(ctx context.Context, lockStrength LockingStrength, key string) (*types.SetupKey, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar setupKey types.SetupKey\n\tresult := tx.\n\t\tTake(&setupKey, GetKeyQueryCondition(s), key)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.PreconditionFailed, \"setup key not found\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get setup key by secret from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get setup key by secret from store\")\n\t}\n\treturn &setupKey, nil\n}\n\nfunc (s *SqlStore) IncrementSetupKeyUsage(ctx context.Context, setupKeyID string) error {\n\tresult := s.db.Model(&types.SetupKey{}).\n\t\tWhere(idQueryCondition, setupKeyID).\n\t\tUpdates(map[string]interface{}{\n\t\t\t\"used_times\": gorm.Expr(\"used_times + 1\"),\n\t\t\t\"last_used\": time.Now(),\n\t\t})\n\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"issue incrementing setup key usage count: %s\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewSetupKeyNotFoundError(setupKeyID)\n\t}\n\n\treturn nil\n}\n\n// AddPeerToAllGroup adds a peer to the 'All' group. Method always needs to run in a transaction\nfunc (s *SqlStore) AddPeerToAllGroup(ctx context.Context, accountID string, peerID string) error {\n\tvar groupID string\n\t_ = s.db.Model(types.Group{}).\n\t\tSelect(\"id\").\n\t\tWhere(\"account_id = ? AND name = ?\", accountID, \"All\").\n\t\tLimit(1).\n\t\tScan(&groupID)\n\n\tif groupID == \"\" {\n\t\treturn status.Errorf(status.NotFound, \"group 'All' not found for account %s\", accountID)\n\t}\n\n\terr := s.db.Clauses(clause.OnConflict{\n\t\tColumns: []clause.Column{{Name: \"group_id\"}, {Name: \"peer_id\"}},\n\t\tDoNothing: true,\n\t}).Create(&types.GroupPeer{\n\t\tAccountID: accountID,\n\t\tGroupID: groupID,\n\t\tPeerID: peerID,\n\t}).Error\n\n\tif err != nil {\n\t\treturn status.Errorf(status.Internal, \"error adding peer to group 'All': %v\", err)\n\t}\n\n\treturn nil\n}\n\n// AddPeerToGroup adds a peer to a group\nfunc (s *SqlStore) AddPeerToGroup(ctx context.Context, accountID, peerID, groupID string) error {\n\tpeer := &types.GroupPeer{\n\t\tAccountID: accountID,\n\t\tGroupID: groupID,\n\t\tPeerID: peerID,\n\t}\n\n\terr := s.db.Clauses(clause.OnConflict{\n\t\tColumns: []clause.Column{{Name: \"group_id\"}, {Name: \"peer_id\"}},\n\t\tDoNothing: true,\n\t}).Create(peer).Error\n\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to add peer %s to group %s for account %s: %v\", peerID, groupID, accountID, err)\n\t\treturn status.Errorf(status.Internal, \"failed to add peer to group\")\n\t}\n\n\treturn nil\n}\n\n// RemovePeerFromGroup removes a peer from a group\nfunc (s *SqlStore) RemovePeerFromGroup(ctx context.Context, peerID string, groupID string) error {\n\terr := s.db.\n\t\tDelete(&types.GroupPeer{}, \"group_id = ? AND peer_id = ?\", groupID, peerID).Error\n\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to remove peer %s from group %s: %v\", peerID, groupID, err)\n\t\treturn status.Errorf(status.Internal, \"failed to remove peer from group\")\n\t}\n\n\treturn nil\n}\n\n// RemovePeerFromAllGroups removes a peer from all groups\nfunc (s *SqlStore) RemovePeerFromAllGroups(ctx context.Context, peerID string) error {\n\terr := s.db.\n\t\tDelete(&types.GroupPeer{}, \"peer_id = ?\", peerID).Error\n\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to remove peer %s from all groups: %v\", peerID, err)\n\t\treturn status.Errorf(status.Internal, \"failed to remove peer from all groups\")\n\t}\n\n\treturn nil\n}\n\n// AddResourceToGroup adds a resource to a group. Method always needs to run n a transaction\nfunc (s *SqlStore) AddResourceToGroup(ctx context.Context, accountId string, groupID string, resource *types.Resource) error {\n\tvar group types.Group\n\tresult := s.db.Where(accountAndIDQueryCondition, accountId, groupID).Take(&group)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn status.NewGroupNotFoundError(groupID)\n\t\t}\n\n\t\treturn status.Errorf(status.Internal, \"issue finding group: %s\", result.Error)\n\t}\n\n\tfor _, res := range group.Resources {\n\t\tif res.ID == resource.ID {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tgroup.Resources = append(group.Resources, *resource)\n\n\tif err := s.db.Save(&group).Error; err != nil {\n\t\treturn status.Errorf(status.Internal, \"issue updating group: %s\", err)\n\t}\n\n\treturn nil\n}\n\n// RemoveResourceFromGroup removes a resource from a group. Method always needs to run in a transaction\nfunc (s *SqlStore) RemoveResourceFromGroup(ctx context.Context, accountId string, groupID string, resourceID string) error {\n\tvar group types.Group\n\tresult := s.db.Where(accountAndIDQueryCondition, accountId, groupID).Take(&group)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn status.NewGroupNotFoundError(groupID)\n\t\t}\n\n\t\treturn status.Errorf(status.Internal, \"issue finding group: %s\", result.Error)\n\t}\n\n\tfor i, res := range group.Resources {\n\t\tif res.ID == resourceID {\n\t\t\tgroup.Resources = append(group.Resources[:i], group.Resources[i+1:]...)\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif err := s.db.Save(&group).Error; err != nil {\n\t\treturn status.Errorf(status.Internal, \"issue updating group: %s\", err)\n\t}\n\n\treturn nil\n}\n\n// GetPeerGroups retrieves all groups assigned to a specific peer in a given account.\nfunc (s *SqlStore) GetPeerGroups(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string) ([]*types.Group, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar groups []*types.Group\n\tquery := tx.\n\t\tJoins(\"JOIN group_peers ON group_peers.group_id = groups.id\").\n\t\tWhere(\"group_peers.peer_id = ?\", peerId).\n\t\tPreload(clause.Associations).\n\t\tFind(&groups)\n\n\tif query.Error != nil {\n\t\treturn nil, query.Error\n\t}\n\n\tfor _, group := range groups {\n\t\tgroup.LoadGroupPeers()\n\t}\n\n\treturn groups, nil\n}\n\n// GetPeerGroupIDs retrieves all group IDs assigned to a specific peer in a given account.\nfunc (s *SqlStore) GetPeerGroupIDs(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string) ([]string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar groupIDs []string\n\tquery := tx.\n\t\tModel(&types.GroupPeer{}).\n\t\tWhere(\"account_id = ? AND peer_id = ?\", accountId, peerId).\n\t\tPluck(\"group_id\", &groupIDs)\n\n\tif query.Error != nil {\n\t\tif errors.Is(query.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"no groups found for peer %s in account %s\", peerId, accountId)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get group IDs for peer %s in account %s: %v\", peerId, accountId, query.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get group IDs for peer from store\")\n\t}\n\n\treturn groupIDs, nil\n}\n\n// GetAccountPeers retrieves peers for an account.\nfunc (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) {\n\tvar peers []*nbpeer.Peer\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\tquery := tx.Where(accountIDCondition, accountID)\n\n\tif nameFilter != \"\" {\n\t\tquery = query.Where(\"name LIKE ?\", \"%\"+nameFilter+\"%\")\n\t}\n\tif ipFilter != \"\" {\n\t\tquery = query.Where(\"ip LIKE ?\", \"%\"+ipFilter+\"%\")\n\t}\n\n\tif err := query.Find(&peers).Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers from store\")\n\t}\n\n\treturn peers, nil\n}\n\n// GetUserPeers retrieves peers for a user.\nfunc (s *SqlStore) GetUserPeers(ctx context.Context, lockStrength LockingStrength, accountID, userID string) ([]*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peers []*nbpeer.Peer\n\n\t// Exclude peers added via setup keys, as they are not user-specific and have an empty user_id.\n\tif userID == \"\" {\n\t\treturn peers, nil\n\t}\n\n\tresult := tx.\n\t\tFind(&peers, \"account_id = ? AND user_id = ?\", accountID, userID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers from store\")\n\t}\n\n\treturn peers, nil\n}\n\nfunc (s *SqlStore) AddPeerToAccount(ctx context.Context, peer *nbpeer.Peer) error {\n\tif err := s.db.Create(peer).Error; err != nil {\n\t\treturn status.Errorf(status.Internal, \"issue adding peer to account: %s\", err)\n\t}\n\n\treturn nil\n}\n\n// GetPeerByID retrieves a peer by its ID and account ID.\nfunc (s *SqlStore) GetPeerByID(ctx context.Context, lockStrength LockingStrength, accountID, peerID string) (*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peer *nbpeer.Peer\n\tresult := tx.\n\t\tTake(&peer, accountAndIDQueryCondition, accountID, peerID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPeerNotFoundError(peerID)\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peer from store\")\n\t}\n\n\treturn peer, nil\n}\n\n// GetPeersByIDs retrieves peers by their IDs and account ID.\nfunc (s *SqlStore) GetPeersByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, peerIDs []string) (map[string]*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peers []*nbpeer.Peer\n\tresult := tx.Find(&peers, accountAndIDsQueryCondition, accountID, peerIDs)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers by ID's from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers by ID's from the store\")\n\t}\n\n\tpeersMap := make(map[string]*nbpeer.Peer)\n\tfor _, peer := range peers {\n\t\tpeersMap[peer.ID] = peer\n\t}\n\n\treturn peersMap, nil\n}\n\n// GetAccountPeersWithExpiration retrieves a list of peers that have login expiration enabled and added by a user.\nfunc (s *SqlStore) GetAccountPeersWithExpiration(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peers []*nbpeer.Peer\n\tresult := tx.\n\t\tWhere(\"login_expiration_enabled = ? AND user_id IS NOT NULL AND user_id != ''\", true).\n\t\tFind(&peers, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers with expiration from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers with expiration from store\")\n\t}\n\n\treturn peers, nil\n}\n\n// GetAccountPeersWithInactivity retrieves a list of peers that have login expiration enabled and added by a user.\nfunc (s *SqlStore) GetAccountPeersWithInactivity(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peers []*nbpeer.Peer\n\tresult := tx.\n\t\tWhere(\"inactivity_expiration_enabled = ? AND user_id IS NOT NULL AND user_id != ''\", true).\n\t\tFind(&peers, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers with inactivity from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers with inactivity from store\")\n\t}\n\n\treturn peers, nil\n}\n\n// GetAllEphemeralPeers retrieves all peers with Ephemeral set to true across all accounts, optimized for batch processing.\nfunc (s *SqlStore) GetAllEphemeralPeers(ctx context.Context, lockStrength LockingStrength) ([]*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar allEphemeralPeers, batchPeers []*nbpeer.Peer\n\tresult := tx.\n\t\tWhere(\"ephemeral = ?\", true).\n\t\tFindInBatches(&batchPeers, 1000, func(tx *gorm.DB, batch int) error {\n\t\t\tallEphemeralPeers = append(allEphemeralPeers, batchPeers...)\n\t\t\treturn nil\n\t\t})\n\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to retrieve ephemeral peers: %s\", result.Error)\n\t\treturn nil, fmt.Errorf(\"failed to retrieve ephemeral peers\")\n\t}\n\n\treturn allEphemeralPeers, nil\n}\n\n// DeletePeer removes a peer from the store.\nfunc (s *SqlStore) DeletePeer(ctx context.Context, accountID string, peerID string) error {\n\tresult := s.db.Delete(&nbpeer.Peer{}, accountAndIDQueryCondition, accountID, peerID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete peer from the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to delete peer from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewPeerNotFoundError(peerID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) IncrementNetworkSerial(ctx context.Context, accountId string) error {\n\tresult := s.db.Model(&types.Account{}).Where(idQueryCondition, accountId).Update(\"network_serial\", gorm.Expr(\"network_serial + 1\"))\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to increment network serial count in store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to increment network serial count in store\")\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) ExecuteInTransaction(ctx context.Context, operation func(store Store) error) error {\n\ttimeoutCtx, cancel := context.WithTimeout(context.Background(), s.transactionTimeout)\n\tdefer cancel()\n\n\tstartTime := time.Now()\n\ttx := s.db.WithContext(timeoutCtx).Begin()\n\tif tx.Error != nil {\n\t\treturn tx.Error\n\t}\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\ttx.Rollback()\n\t\t\tpanic(r)\n\t\t}\n\t}()\n\n\tif s.storeEngine == types.PostgresStoreEngine {\n\t\tif err := tx.Exec(\"SET LOCAL statement_timeout = '1min'\").Error; err != nil {\n\t\t\ttx.Rollback()\n\t\t\treturn fmt.Errorf(\"failed to set statement timeout: %w\", err)\n\t\t}\n\t\tif err := tx.Exec(\"SET LOCAL lock_timeout = '1min'\").Error; err != nil {\n\t\t\ttx.Rollback()\n\t\t\treturn fmt.Errorf(\"failed to set lock timeout: %w\", err)\n\t\t}\n\t}\n\n\t// For MySQL, disable FK checks within this transaction to avoid deadlocks\n\t// This is session-scoped and doesn't require SUPER privileges\n\tif s.storeEngine == types.MysqlStoreEngine {\n\t\tif err := tx.Exec(\"SET FOREIGN_KEY_CHECKS = 0\").Error; err != nil {\n\t\t\ttx.Rollback()\n\t\t\treturn fmt.Errorf(\"failed to disable FK checks: %w\", err)\n\t\t}\n\t}\n\n\trepo := s.withTx(tx)\n\terr := operation(repo)\n\tif err != nil {\n\t\ttx.Rollback()\n\t\tif errors.Is(err, context.DeadlineExceeded) || errors.Is(timeoutCtx.Err(), context.DeadlineExceeded) {\n\t\t\tlog.WithContext(ctx).Warnf(\"transaction exceeded %s timeout after %v, stack: %s\", s.transactionTimeout, time.Since(startTime), debug.Stack())\n\t\t}\n\t\treturn err\n\t}\n\n\t// Re-enable FK checks before commit (optional, as transaction end resets it)\n\tif s.storeEngine == types.MysqlStoreEngine {\n\t\tif err := tx.Exec(\"SET FOREIGN_KEY_CHECKS = 1\").Error; err != nil {\n\t\t\ttx.Rollback()\n\t\t\treturn fmt.Errorf(\"failed to re-enable FK checks: %w\", err)\n\t\t}\n\t}\n\n\terr = tx.Commit().Error\n\tif err != nil {\n\t\tif errors.Is(err, context.DeadlineExceeded) || errors.Is(timeoutCtx.Err(), context.DeadlineExceeded) {\n\t\t\tlog.WithContext(ctx).Warnf(\"transaction commit exceeded %s timeout after %v, stack: %s\", s.transactionTimeout, time.Since(startTime), debug.Stack())\n\t\t}\n\t\treturn err\n\t}\n\n\tlog.WithContext(ctx).Tracef(\"transaction took %v\", time.Since(startTime))\n\tif s.metrics != nil {\n\t\ts.metrics.StoreMetrics().CountTransactionDuration(time.Since(startTime))\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) withTx(tx *gorm.DB) Store {\n\treturn &SqlStore{\n\t\tdb: tx,\n\t\tstoreEngine: s.storeEngine,\n\t\tfieldEncrypt: s.fieldEncrypt,\n\t}\n}\n\n// transaction wraps a GORM transaction with MySQL-specific FK checks handling\n// Use this instead of db.Transaction() directly to avoid deadlocks on MySQL/Aurora\nfunc (s *SqlStore) transaction(fn func(*gorm.DB) error) error {\n\treturn s.db.Transaction(func(tx *gorm.DB) error {\n\t\t// For MySQL, disable FK checks within this transaction to avoid deadlocks\n\t\t// This is session-scoped and doesn't require SUPER privileges\n\t\tif s.storeEngine == types.MysqlStoreEngine {\n\t\t\tif err := tx.Exec(\"SET FOREIGN_KEY_CHECKS = 0\").Error; err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to disable FK checks: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\terr := fn(tx)\n\n\t\t// Re-enable FK checks before commit (optional, as transaction end resets it)\n\t\tif s.storeEngine == types.MysqlStoreEngine && err == nil {\n\t\t\tif fkErr := tx.Exec(\"SET FOREIGN_KEY_CHECKS = 1\").Error; fkErr != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to re-enable FK checks: %w\", fkErr)\n\t\t\t}\n\t\t}\n\n\t\treturn err\n\t})\n}\n\nfunc (s *SqlStore) GetDB() *gorm.DB {\n\treturn s.db\n}\n\n// SetFieldEncrypt sets the field encryptor for encrypting sensitive user data.\nfunc (s *SqlStore) SetFieldEncrypt(enc *crypt.FieldEncrypt) {\n\ts.fieldEncrypt = enc\n}\n\nfunc (s *SqlStore) GetAccountDNSSettings(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.DNSSettings, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountDNSSettings types.AccountDNSSettings\n\tresult := tx.Model(&types.Account{}).\n\t\tTake(&accountDNSSettings, idQueryCondition, accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get dns settings from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get dns settings from store\")\n\t}\n\treturn &accountDNSSettings.DNSSettings, nil\n}\n\n// AccountExists checks whether an account exists by the given ID.\nfunc (s *SqlStore) AccountExists(ctx context.Context, lockStrength LockingStrength, id string) (bool, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountID string\n\tresult := tx.Model(&types.Account{}).\n\t\tSelect(\"id\").Take(&accountID, idQueryCondition, id)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, result.Error\n\t}\n\n\treturn accountID != \"\", nil\n}\n\n// GetAccountDomainAndCategory retrieves the Domain and DomainCategory fields for an account based on the given accountID.\nfunc (s *SqlStore) GetAccountDomainAndCategory(ctx context.Context, lockStrength LockingStrength, accountID string) (string, string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar account types.Account\n\tresult := tx.Model(&types.Account{}).Select(\"domain\", \"domain_category\").\n\t\tWhere(idQueryCondition, accountID).Take(&account)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", \"\", status.Errorf(status.NotFound, \"account not found\")\n\t\t}\n\t\treturn \"\", \"\", status.Errorf(status.Internal, \"failed to get domain category from store: %v\", result.Error)\n\t}\n\n\treturn account.Domain, account.DomainCategory, nil\n}\n\n// GetGroupByID retrieves a group by ID and account ID.\nfunc (s *SqlStore) GetGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) (*types.Group, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar group *types.Group\n\tresult := tx.Preload(clause.Associations).Take(&group, accountAndIDQueryCondition, accountID, groupID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewGroupNotFoundError(groupID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get group from store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get group from store\")\n\t}\n\n\tgroup.LoadGroupPeers()\n\n\treturn group, nil\n}\n\n// GetGroupByName retrieves a group by name and account ID.\nfunc (s *SqlStore) GetGroupByName(ctx context.Context, lockStrength LockingStrength, accountID, groupName string) (*types.Group, error) {\n\ttx := s.db\n\n\tvar group types.Group\n\n\t// TODO: This fix is accepted for now, but if we need to handle this more frequently\n\t// we may need to reconsider changing the types.\n\tquery := tx.Preload(clause.Associations)\n\n\tresult := query.\n\t\tModel(&types.Group{}).\n\t\tJoins(\"LEFT JOIN group_peers ON group_peers.group_id = groups.id\").\n\t\tWhere(\"groups.account_id = ? AND groups.name = ?\", accountID, groupName).\n\t\tGroup(\"groups.id\").\n\t\tOrder(\"COUNT(group_peers.peer_id) DESC\").\n\t\tLimit(1).\n\t\tFirst(&group)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewGroupNotFoundError(groupName)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get group by name from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get group by name from store\")\n\t}\n\n\tgroup.LoadGroupPeers()\n\n\treturn &group, nil\n}\n\n// GetGroupsByIDs retrieves groups by their IDs and account ID.\nfunc (s *SqlStore) GetGroupsByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, groupIDs []string) (map[string]*types.Group, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar groups []*types.Group\n\tresult := tx.Preload(clause.Associations).Find(&groups, accountAndIDsQueryCondition, accountID, groupIDs)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get groups by ID's from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get groups by ID's from store\")\n\t}\n\n\tgroupsMap := make(map[string]*types.Group)\n\tfor _, group := range groups {\n\t\tgroup.LoadGroupPeers()\n\t\tgroupsMap[group.ID] = group\n\t}\n\n\treturn groupsMap, nil\n}\n\n// CreateGroup creates a group in the store.\nfunc (s *SqlStore) CreateGroup(ctx context.Context, group *types.Group) error {\n\tif group == nil {\n\t\treturn status.Errorf(status.InvalidArgument, \"group is nil\")\n\t}\n\n\tif err := s.db.Omit(clause.Associations).Create(group).Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save group to store: %v\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save group to store\")\n\t}\n\n\treturn nil\n}\n\n// UpdateGroup updates a group in the store.\nfunc (s *SqlStore) UpdateGroup(ctx context.Context, group *types.Group) error {\n\tif group == nil {\n\t\treturn status.Errorf(status.InvalidArgument, \"group is nil\")\n\t}\n\n\tif err := s.db.Omit(clause.Associations).Save(group).Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save group to store: %v\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save group to store\")\n\t}\n\n\treturn nil\n}\n\n// DeleteGroup deletes a group from the database.\nfunc (s *SqlStore) DeleteGroup(ctx context.Context, accountID, groupID string) error {\n\tresult := s.db.Select(clause.Associations).\n\t\tDelete(&types.Group{}, accountAndIDQueryCondition, accountID, groupID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete group from store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete group from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewGroupNotFoundError(groupID)\n\t}\n\n\treturn nil\n}\n\n// DeleteGroups deletes groups from the database.\nfunc (s *SqlStore) DeleteGroups(ctx context.Context, accountID string, groupIDs []string) error {\n\tresult := s.db.Select(clause.Associations).\n\t\tDelete(&types.Group{}, accountAndIDsQueryCondition, accountID, groupIDs)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete groups from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete groups from store\")\n\t}\n\n\treturn nil\n}\n\n// GetAccountPolicies retrieves policies for an account.\nfunc (s *SqlStore) GetAccountPolicies(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.Policy, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar policies []*types.Policy\n\tresult := tx.\n\t\tPreload(clause.Associations).Find(&policies, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get policies from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get policies from store\")\n\t}\n\n\treturn policies, nil\n}\n\n// GetPolicyByID retrieves a policy by its ID and account ID.\nfunc (s *SqlStore) GetPolicyByID(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) (*types.Policy, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar policy *types.Policy\n\n\tresult := tx.Preload(clause.Associations).\n\t\tTake(&policy, accountAndIDQueryCondition, accountID, policyID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPolicyNotFoundError(policyID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get policy from store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get policy from store\")\n\t}\n\n\treturn policy, nil\n}\n\nfunc (s *SqlStore) CreatePolicy(ctx context.Context, policy *types.Policy) error {\n\tresult := s.db.Create(policy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create policy in store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create policy in store\")\n\t}\n\n\treturn nil\n}\n\n// SavePolicy saves a policy to the database.\nfunc (s *SqlStore) SavePolicy(ctx context.Context, policy *types.Policy) error {\n\tresult := s.db.Session(&gorm.Session{FullSaveAssociations: true}).Save(policy)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save policy to the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save policy to store\")\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) DeletePolicy(ctx context.Context, accountID, policyID string) error {\n\treturn s.transaction(func(tx *gorm.DB) error {\n\t\tif err := tx.Where(\"policy_id = ?\", policyID).Delete(&types.PolicyRule{}).Error; err != nil {\n\t\t\treturn fmt.Errorf(\"delete policy rules: %w\", err)\n\t\t}\n\n\t\tresult := tx.\n\t\t\tWhere(accountAndIDQueryCondition, accountID, policyID).\n\t\t\tDelete(&types.Policy{})\n\n\t\tif err := result.Error; err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"failed to delete policy from store: %s\", err)\n\t\t\treturn status.Errorf(status.Internal, \"failed to delete policy from store\")\n\t\t}\n\n\t\tif result.RowsAffected == 0 {\n\t\t\treturn status.NewPolicyNotFoundError(policyID)\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\nfunc (s *SqlStore) GetPolicyRulesByResourceID(ctx context.Context, lockStrength LockingStrength, accountID string, resourceID string) ([]*types.PolicyRule, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar policyRules []*types.PolicyRule\n\tresourceIDPattern := `%\"ID\":\"` + resourceID + `\"%`\n\tresult := tx.Where(\"source_resource LIKE ? OR destination_resource LIKE ?\", resourceIDPattern, resourceIDPattern).\n\t\tFind(&policyRules)\n\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get policy rules for resource id from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get policy rules for resource id from store\")\n\t}\n\n\treturn policyRules, nil\n}\n\n// GetAccountPostureChecks retrieves posture checks for an account.\nfunc (s *SqlStore) GetAccountPostureChecks(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*posture.Checks, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar postureChecks []*posture.Checks\n\tresult := tx.Find(&postureChecks, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get posture checks from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get posture checks from store\")\n\t}\n\n\treturn postureChecks, nil\n}\n\n// GetPostureChecksByID retrieves posture checks by their ID and account ID.\nfunc (s *SqlStore) GetPostureChecksByID(ctx context.Context, lockStrength LockingStrength, accountID, postureChecksID string) (*posture.Checks, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar postureCheck *posture.Checks\n\tresult := tx.\n\t\tTake(&postureCheck, accountAndIDQueryCondition, accountID, postureChecksID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPostureChecksNotFoundError(postureChecksID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get posture check from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get posture check from store\")\n\t}\n\n\treturn postureCheck, nil\n}\n\n// GetPostureChecksByIDs retrieves posture checks by their IDs and account ID.\nfunc (s *SqlStore) GetPostureChecksByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, postureChecksIDs []string) (map[string]*posture.Checks, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar postureChecks []*posture.Checks\n\tresult := tx.Find(&postureChecks, accountAndIDsQueryCondition, accountID, postureChecksIDs)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get posture checks by ID's from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get posture checks by ID's from store\")\n\t}\n\n\tpostureChecksMap := make(map[string]*posture.Checks)\n\tfor _, postureCheck := range postureChecks {\n\t\tpostureChecksMap[postureCheck.ID] = postureCheck\n\t}\n\n\treturn postureChecksMap, nil\n}\n\n// SavePostureChecks saves a posture checks to the database.\nfunc (s *SqlStore) SavePostureChecks(ctx context.Context, postureCheck *posture.Checks) error {\n\tresult := s.db.Save(postureCheck)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save posture checks to store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save posture checks to store\")\n\t}\n\n\treturn nil\n}\n\n// DeletePostureChecks deletes a posture checks from the database.\nfunc (s *SqlStore) DeletePostureChecks(ctx context.Context, accountID, postureChecksID string) error {\n\tresult := s.db.Delete(&posture.Checks{}, accountAndIDQueryCondition, accountID, postureChecksID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete posture checks from store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete posture checks from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewPostureChecksNotFoundError(postureChecksID)\n\t}\n\n\treturn nil\n}\n\n// GetAccountRoutes retrieves network routes for an account.\nfunc (s *SqlStore) GetAccountRoutes(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*route.Route, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar routes []*route.Route\n\tresult := tx.Find(&routes, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get routes from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get routes from store\")\n\t}\n\n\treturn routes, nil\n}\n\n// GetRouteByID retrieves a route by its ID and account ID.\nfunc (s *SqlStore) GetRouteByID(ctx context.Context, lockStrength LockingStrength, accountID string, routeID string) (*route.Route, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar route *route.Route\n\tresult := tx.Take(&route, accountAndIDQueryCondition, accountID, routeID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewRouteNotFoundError(routeID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get route from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get route from store\")\n\t}\n\n\treturn route, nil\n}\n\n// SaveRoute saves a route to the database.\nfunc (s *SqlStore) SaveRoute(ctx context.Context, route *route.Route) error {\n\tresult := s.db.Save(route)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save route to the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save route to store\")\n\t}\n\n\treturn nil\n}\n\n// DeleteRoute deletes a route from the database.\nfunc (s *SqlStore) DeleteRoute(ctx context.Context, accountID, routeID string) error {\n\tresult := s.db.Delete(&route.Route{}, accountAndIDQueryCondition, accountID, routeID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete route from the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to delete route from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewRouteNotFoundError(routeID)\n\t}\n\n\treturn nil\n}\n\n// GetAccountSetupKeys retrieves setup keys for an account.\nfunc (s *SqlStore) GetAccountSetupKeys(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.SetupKey, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar setupKeys []*types.SetupKey\n\tresult := tx.\n\t\tFind(&setupKeys, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get setup keys from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get setup keys from store\")\n\t}\n\n\treturn setupKeys, nil\n}\n\n// GetSetupKeyByID retrieves a setup key by its ID and account ID.\nfunc (s *SqlStore) GetSetupKeyByID(ctx context.Context, lockStrength LockingStrength, accountID, setupKeyID string) (*types.SetupKey, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar setupKey *types.SetupKey\n\tresult := tx.Take(&setupKey, accountAndIDQueryCondition, accountID, setupKeyID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewSetupKeyNotFoundError(setupKeyID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get setup key from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get setup key from store\")\n\t}\n\n\treturn setupKey, nil\n}\n\n// SaveSetupKey saves a setup key to the database.\nfunc (s *SqlStore) SaveSetupKey(ctx context.Context, setupKey *types.SetupKey) error {\n\tresult := s.db.Save(setupKey)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save setup key to store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save setup key to store\")\n\t}\n\n\treturn nil\n}\n\n// DeleteSetupKey deletes a setup key from the database.\nfunc (s *SqlStore) DeleteSetupKey(ctx context.Context, accountID, keyID string) error {\n\tresult := s.db.Delete(&types.SetupKey{}, accountAndIDQueryCondition, accountID, keyID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete setup key from store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete setup key from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewSetupKeyNotFoundError(keyID)\n\t}\n\n\treturn nil\n}\n\n// GetAccountNameServerGroups retrieves name server groups for an account.\nfunc (s *SqlStore) GetAccountNameServerGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbdns.NameServerGroup, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar nsGroups []*nbdns.NameServerGroup\n\tresult := tx.Find(&nsGroups, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get name server groups from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get name server groups from store\")\n\t}\n\n\treturn nsGroups, nil\n}\n\n// GetNameServerGroupByID retrieves a name server group by its ID and account ID.\nfunc (s *SqlStore) GetNameServerGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, nsGroupID string) (*nbdns.NameServerGroup, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar nsGroup *nbdns.NameServerGroup\n\tresult := tx.\n\t\tTake(&nsGroup, accountAndIDQueryCondition, accountID, nsGroupID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewNameServerGroupNotFoundError(nsGroupID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get name server group from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get name server group from store\")\n\t}\n\n\treturn nsGroup, nil\n}\n\n// SaveNameServerGroup saves a name server group to the database.\nfunc (s *SqlStore) SaveNameServerGroup(ctx context.Context, nameServerGroup *nbdns.NameServerGroup) error {\n\tresult := s.db.Save(nameServerGroup)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save name server group to the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save name server group to store\")\n\t}\n\treturn nil\n}\n\n// DeleteNameServerGroup deletes a name server group from the database.\nfunc (s *SqlStore) DeleteNameServerGroup(ctx context.Context, accountID, nsGroupID string) error {\n\tresult := s.db.Delete(&nbdns.NameServerGroup{}, accountAndIDQueryCondition, accountID, nsGroupID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete name server group from the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to delete name server group from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewNameServerGroupNotFoundError(nsGroupID)\n\t}\n\n\treturn nil\n}\n\n// SaveDNSSettings saves the DNS settings to the store.\nfunc (s *SqlStore) SaveDNSSettings(ctx context.Context, accountID string, settings *types.DNSSettings) error {\n\tresult := s.db.Model(&types.Account{}).\n\t\tWhere(idQueryCondition, accountID).Updates(&types.AccountDNSSettings{DNSSettings: *settings})\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save dns settings to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save dns settings to store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewAccountNotFoundError(accountID)\n\t}\n\n\treturn nil\n}\n\n// SaveAccountSettings stores the account settings in DB.\nfunc (s *SqlStore) SaveAccountSettings(ctx context.Context, accountID string, settings *types.Settings) error {\n\tresult := s.db.Model(&types.Account{}).\n\t\tSelect(\"*\").Where(idQueryCondition, accountID).Updates(&types.AccountSettings{Settings: settings})\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save account settings to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save account settings to store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewAccountNotFoundError(accountID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetAccountNetworks(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*networkTypes.Network, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar networks []*networkTypes.Network\n\tresult := tx.Find(&networks, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get networks from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get networks from store\")\n\t}\n\n\treturn networks, nil\n}\n\nfunc (s *SqlStore) GetNetworkByID(ctx context.Context, lockStrength LockingStrength, accountID, networkID string) (*networkTypes.Network, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar network *networkTypes.Network\n\tresult := tx.Take(&network, accountAndIDQueryCondition, accountID, networkID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewNetworkNotFoundError(networkID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network from store\")\n\t}\n\n\treturn network, nil\n}\n\nfunc (s *SqlStore) SaveNetwork(ctx context.Context, network *networkTypes.Network) error {\n\tresult := s.db.Save(network)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save network to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save network to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteNetwork(ctx context.Context, accountID, networkID string) error {\n\tresult := s.db.Delete(&networkTypes.Network{}, accountAndIDQueryCondition, accountID, networkID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete network from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete network from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewNetworkNotFoundError(networkID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetNetworkRoutersByNetID(ctx context.Context, lockStrength LockingStrength, accountID, netID string) ([]*routerTypes.NetworkRouter, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netRouters []*routerTypes.NetworkRouter\n\tresult := tx.\n\t\tFind(&netRouters, \"account_id = ? AND network_id = ?\", accountID, netID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network routers from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network routers from store\")\n\t}\n\n\treturn netRouters, nil\n}\n\nfunc (s *SqlStore) GetNetworkRoutersByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*routerTypes.NetworkRouter, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netRouters []*routerTypes.NetworkRouter\n\tresult := tx.\n\t\tFind(&netRouters, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network routers from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network routers from store\")\n\t}\n\n\treturn netRouters, nil\n}\n\nfunc (s *SqlStore) GetNetworkRouterByID(ctx context.Context, lockStrength LockingStrength, accountID, routerID string) (*routerTypes.NetworkRouter, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netRouter *routerTypes.NetworkRouter\n\tresult := tx.\n\t\tTake(&netRouter, accountAndIDQueryCondition, accountID, routerID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewNetworkRouterNotFoundError(routerID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network router from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network router from store\")\n\t}\n\n\treturn netRouter, nil\n}\n\nfunc (s *SqlStore) SaveNetworkRouter(ctx context.Context, router *routerTypes.NetworkRouter) error {\n\tresult := s.db.Save(router)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save network router to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save network router to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteNetworkRouter(ctx context.Context, accountID, routerID string) error {\n\tresult := s.db.Delete(&routerTypes.NetworkRouter{}, accountAndIDQueryCondition, accountID, routerID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete network router from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete network router from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewNetworkRouterNotFoundError(routerID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetNetworkResourcesByNetID(ctx context.Context, lockStrength LockingStrength, accountID, networkID string) ([]*resourceTypes.NetworkResource, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netResources []*resourceTypes.NetworkResource\n\tresult := tx.\n\t\tFind(&netResources, \"account_id = ? AND network_id = ?\", accountID, networkID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network resources from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network resources from store\")\n\t}\n\n\treturn netResources, nil\n}\n\nfunc (s *SqlStore) GetNetworkResourcesByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*resourceTypes.NetworkResource, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netResources []*resourceTypes.NetworkResource\n\tresult := tx.\n\t\tFind(&netResources, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network resources from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network resources from store\")\n\t}\n\n\treturn netResources, nil\n}\n\nfunc (s *SqlStore) GetNetworkResourceByID(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) (*resourceTypes.NetworkResource, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netResources *resourceTypes.NetworkResource\n\tresult := tx.\n\t\tTake(&netResources, accountAndIDQueryCondition, accountID, resourceID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewNetworkResourceNotFoundError(resourceID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network resource from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network resource from store\")\n\t}\n\n\treturn netResources, nil\n}\n\nfunc (s *SqlStore) GetNetworkResourceByName(ctx context.Context, lockStrength LockingStrength, accountID, resourceName string) (*resourceTypes.NetworkResource, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netResources *resourceTypes.NetworkResource\n\tresult := tx.\n\t\tTake(&netResources, \"account_id = ? AND name = ?\", accountID, resourceName)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewNetworkResourceNotFoundError(resourceName)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network resource from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network resource from store\")\n\t}\n\n\treturn netResources, nil\n}\n\nfunc (s *SqlStore) SaveNetworkResource(ctx context.Context, resource *resourceTypes.NetworkResource) error {\n\tresult := s.db.Save(resource)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save network resource to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save network resource to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteNetworkResource(ctx context.Context, accountID, resourceID string) error {\n\tresult := s.db.Delete(&resourceTypes.NetworkResource{}, accountAndIDQueryCondition, accountID, resourceID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete network resource from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete network resource from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewNetworkResourceNotFoundError(resourceID)\n\t}\n\n\treturn nil\n}\n\n// GetPATByHashedToken returns a PersonalAccessToken by its hashed token.\nfunc (s *SqlStore) GetPATByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken string) (*types.PersonalAccessToken, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar pat types.PersonalAccessToken\n\tresult := tx.Take(&pat, \"hashed_token = ?\", hashedToken)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPATNotFoundError(hashedToken)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get pat by hash from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get pat by hash from store\")\n\t}\n\n\treturn &pat, nil\n}\n\n// GetPATByID retrieves a personal access token by its ID and user ID.\nfunc (s *SqlStore) GetPATByID(ctx context.Context, lockStrength LockingStrength, userID string, patID string) (*types.PersonalAccessToken, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar pat types.PersonalAccessToken\n\tresult := tx.\n\t\tTake(&pat, \"id = ? AND user_id = ?\", patID, userID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPATNotFoundError(patID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get pat from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get pat from store\")\n\t}\n\n\treturn &pat, nil\n}\n\n// GetUserPATs retrieves personal access tokens for a user.\nfunc (s *SqlStore) GetUserPATs(ctx context.Context, lockStrength LockingStrength, userID string) ([]*types.PersonalAccessToken, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar pats []*types.PersonalAccessToken\n\tresult := tx.Find(&pats, \"user_id = ?\", userID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get user pat's from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get user pat's from store\")\n\t}\n\n\treturn pats, nil\n}\n\n// MarkPATUsed marks a personal access token as used.\nfunc (s *SqlStore) MarkPATUsed(ctx context.Context, patID string) error {\n\tpatCopy := types.PersonalAccessToken{\n\t\tLastUsed: util.ToPtr(time.Now().UTC()),\n\t}\n\n\tfieldsToUpdate := []string{\"last_used\"}\n\tresult := s.db.Select(fieldsToUpdate).\n\t\tWhere(idQueryCondition, patID).Updates(&patCopy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to mark pat as used: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to mark pat as used\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewPATNotFoundError(patID)\n\t}\n\n\treturn nil\n}\n\n// SavePAT saves a personal access token to the database.\nfunc (s *SqlStore) SavePAT(ctx context.Context, pat *types.PersonalAccessToken) error {\n\tresult := s.db.Save(pat)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save pat to the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save pat to store\")\n\t}\n\n\treturn nil\n}\n\n// DeletePAT deletes a personal access token from the database.\nfunc (s *SqlStore) DeletePAT(ctx context.Context, userID, patID string) error {\n\tresult := s.db.Delete(&types.PersonalAccessToken{}, \"user_id = ? AND id = ?\", userID, patID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete pat from the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to delete pat from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewPATNotFoundError(patID)\n\t}\n\n\treturn nil\n}\n\n// GetProxyAccessTokenByHashedToken retrieves a proxy access token by its hashed value.\nfunc (s *SqlStore) GetProxyAccessTokenByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken types.HashedProxyToken) (*types.ProxyAccessToken, error) {\n\ttx := s.db.WithContext(ctx)\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar token types.ProxyAccessToken\n\tresult := tx.Take(&token, \"hashed_token = ?\", hashedToken)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"proxy access token not found\")\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"get proxy access token: %v\", result.Error)\n\t}\n\n\treturn &token, nil\n}\n\n// GetAllProxyAccessTokens retrieves all proxy access tokens.\nfunc (s *SqlStore) GetAllProxyAccessTokens(ctx context.Context, lockStrength LockingStrength) ([]*types.ProxyAccessToken, error) {\n\ttx := s.db.WithContext(ctx)\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar tokens []*types.ProxyAccessToken\n\tresult := tx.Find(&tokens)\n\tif result.Error != nil {\n\t\treturn nil, status.Errorf(status.Internal, \"get proxy access tokens: %v\", result.Error)\n\t}\n\n\treturn tokens, nil\n}\n\n// SaveProxyAccessToken saves a proxy access token to the database.\nfunc (s *SqlStore) SaveProxyAccessToken(ctx context.Context, token *types.ProxyAccessToken) error {\n\tif result := s.db.WithContext(ctx).Create(token); result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"save proxy access token: %v\", result.Error)\n\t}\n\treturn nil\n}\n\n// RevokeProxyAccessToken revokes a proxy access token by its ID.\nfunc (s *SqlStore) RevokeProxyAccessToken(ctx context.Context, tokenID string) error {\n\tresult := s.db.WithContext(ctx).Model(&types.ProxyAccessToken{}).Where(idQueryCondition, tokenID).Update(\"revoked\", true)\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"revoke proxy access token: %v\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"proxy access token not found\")\n\t}\n\n\treturn nil\n}\n\n// MarkProxyAccessTokenUsed updates the last used timestamp for a proxy access token.\nfunc (s *SqlStore) MarkProxyAccessTokenUsed(ctx context.Context, tokenID string) error {\n\tresult := s.db.WithContext(ctx).Model(&types.ProxyAccessToken{}).\n\t\tWhere(idQueryCondition, tokenID).\n\t\tUpdate(\"last_used\", time.Now().UTC())\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"mark proxy access token as used: %v\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"proxy access token not found\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetPeerByIP(ctx context.Context, lockStrength LockingStrength, accountID string, ip net.IP) (*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tjsonValue := fmt.Sprintf(`\"%s\"`, ip.String())\n\n\tvar peer nbpeer.Peer\n\tresult := tx.\n\t\tTake(&peer, \"account_id = ? AND ip = ?\", accountID, jsonValue)\n\tif result.Error != nil {\n\t\t// no logging here\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peer from store\")\n\t}\n\n\treturn &peer, nil\n}\n\nfunc (s *SqlStore) GetPeerIdByLabel(ctx context.Context, lockStrength LockingStrength, accountID string, hostname string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peerID string\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tSelect(\"id\").\n\t\t// Where(\" = ?\", hostname).\n\t\tWhere(\"account_id = ? AND dns_label = ?\", accountID, hostname).\n\t\tLimit(1).\n\t\tScan(&peerID)\n\n\tif peerID == \"\" {\n\t\treturn \"\", gorm.ErrRecordNotFound\n\t}\n\n\treturn peerID, result.Error\n}\n\nfunc (s *SqlStore) CountAccountsByPrivateDomain(ctx context.Context, domain string) (int64, error) {\n\tvar count int64\n\tresult := s.db.Model(&types.Account{}).\n\t\tWhere(\"domain = ? AND domain_category = ?\",\n\t\t\tstrings.ToLower(domain), types.PrivateCategory,\n\t\t).Count(&count)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to count accounts by private domain %s: %s\", domain, result.Error)\n\t\treturn 0, status.Errorf(status.Internal, \"failed to count accounts by private domain\")\n\t}\n\n\treturn count, nil\n}\n\nfunc (s *SqlStore) GetAccountGroupPeers(ctx context.Context, lockStrength LockingStrength, accountID string) (map[string]map[string]struct{}, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peers []types.GroupPeer\n\tresult := tx.Find(&peers, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get account group peers from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get account group peers from store\")\n\t}\n\n\tgroupPeers := make(map[string]map[string]struct{})\n\tfor _, peer := range peers {\n\t\tif _, exists := groupPeers[peer.GroupID]; !exists {\n\t\t\tgroupPeers[peer.GroupID] = make(map[string]struct{})\n\t\t}\n\t\tgroupPeers[peer.GroupID][peer.PeerID] = struct{}{}\n\t}\n\n\treturn groupPeers, nil\n}\n\nfunc (s *SqlStore) IsPrimaryAccount(ctx context.Context, accountID string) (bool, string, error) {\n\tvar info types.PrimaryAccountInfo\n\tresult := s.db.Model(&types.Account{}).\n\t\tSelect(\"is_domain_primary_account, domain\").\n\t\tWhere(idQueryCondition, accountID).\n\t\tTake(&info)\n\n\tif result.Error != nil {\n\t\treturn false, \"\", status.Errorf(status.Internal, \"failed to get account info: %v\", result.Error)\n\t}\n\n\treturn info.IsDomainPrimaryAccount, info.Domain, nil\n}\n\nfunc (s *SqlStore) MarkAccountPrimary(ctx context.Context, accountID string) error {\n\tresult := s.db.Model(&types.Account{}).\n\t\tWhere(idQueryCondition, accountID).\n\t\tUpdate(\"is_domain_primary_account\", true)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to mark account as primary: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to mark account as primary\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewAccountNotFoundError(accountID)\n\t}\n\n\treturn nil\n}\n\ntype accountNetworkPatch struct {\n\tNetwork *types.Network `gorm:\"embedded;embeddedPrefix:network_\"`\n}\n\nfunc (s *SqlStore) UpdateAccountNetwork(ctx context.Context, accountID string, ipNet net.IPNet) error {\n\tpatch := accountNetworkPatch{\n\t\tNetwork: &types.Network{Net: ipNet},\n\t}\n\n\tresult := s.db.\n\t\tModel(&types.Account{}).\n\t\tWhere(idQueryCondition, accountID).\n\t\tUpdates(&patch)\n\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update account network: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to update account network\")\n\t}\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewAccountNotFoundError(accountID)\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) GetPeersByGroupIDs(ctx context.Context, accountID string, groupIDs []string) ([]*nbpeer.Peer, error) {\n\tif len(groupIDs) == 0 {\n\t\treturn []*nbpeer.Peer{}, nil\n\t}\n\n\tvar peers []*nbpeer.Peer\n\tpeerIDsSubquery := s.db.Model(&types.GroupPeer{}).\n\t\tSelect(\"DISTINCT peer_id\").\n\t\tWhere(\"account_id = ? AND group_id IN ?\", accountID, groupIDs)\n\n\tresult := s.db.Where(\"id IN (?)\", peerIDsSubquery).Find(&peers)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers by group IDs: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers by group IDs\")\n\t}\n\n\treturn peers, nil\n}\n\nfunc (s *SqlStore) GetUserIDByPeerKey(ctx context.Context, lockStrength LockingStrength, peerKey string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar userID string\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tSelect(\"user_id\").\n\t\tTake(&userID, GetKeyQueryCondition(s), peerKey)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"peer not found: index lookup failed\")\n\t\t}\n\t\treturn \"\", status.Errorf(status.Internal, \"failed to get user ID by peer key\")\n\t}\n\n\treturn userID, nil\n}\n\nfunc (s *SqlStore) CreateZone(ctx context.Context, zone *zones.Zone) error {\n\tresult := s.db.Create(zone)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create zone to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create zone to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) UpdateZone(ctx context.Context, zone *zones.Zone) error {\n\tresult := s.db.Select(\"*\").Save(zone)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update zone to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to update zone to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteZone(ctx context.Context, accountID, zoneID string) error {\n\tresult := s.db.Delete(&zones.Zone{}, accountAndIDQueryCondition, accountID, zoneID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete zone from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete zone from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewZoneNotFoundError(zoneID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetZoneByID(ctx context.Context, lockStrength LockingStrength, accountID, zoneID string) (*zones.Zone, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar zone *zones.Zone\n\tresult := tx.Preload(\"Records\").Take(&zone, accountAndIDQueryCondition, accountID, zoneID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewZoneNotFoundError(zoneID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get zone from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get zone from store\")\n\t}\n\n\treturn zone, nil\n}\n\nfunc (s *SqlStore) GetZoneByDomain(ctx context.Context, accountID, domain string) (*zones.Zone, error) {\n\tvar zone *zones.Zone\n\tresult := s.db.Where(\"account_id = ? AND domain = ?\", accountID, domain).First(&zone)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewZoneNotFoundError(domain)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get zone by domain from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get zone by domain from store\")\n\t}\n\n\treturn zone, nil\n}\n\nfunc (s *SqlStore) GetAccountZones(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*zones.Zone, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar zones []*zones.Zone\n\tresult := tx.Preload(\"Records\").Find(&zones, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get zones from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get zones from store\")\n\t}\n\n\treturn zones, nil\n}\n\nfunc (s *SqlStore) CreateDNSRecord(ctx context.Context, record *records.Record) error {\n\tresult := s.db.Create(record)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create dns record to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create dns record to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) UpdateDNSRecord(ctx context.Context, record *records.Record) error {\n\tresult := s.db.Select(\"*\").Save(record)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update dns record to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to update dns record to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteDNSRecord(ctx context.Context, accountID, zoneID, recordID string) error {\n\tresult := s.db.Delete(&records.Record{}, \"account_id = ? AND zone_id = ? AND id = ?\", accountID, zoneID, recordID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete dns record from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete dns record from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewDNSRecordNotFoundError(recordID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetDNSRecordByID(ctx context.Context, lockStrength LockingStrength, accountID, zoneID, recordID string) (*records.Record, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar record *records.Record\n\tresult := tx.Where(\"account_id = ? AND zone_id = ? AND id = ?\", accountID, zoneID, recordID).Take(&record)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewDNSRecordNotFoundError(recordID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get dns record from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get dns record from store\")\n\t}\n\n\treturn record, nil\n}\n\nfunc (s *SqlStore) GetZoneDNSRecords(ctx context.Context, lockStrength LockingStrength, accountID, zoneID string) ([]*records.Record, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar recordsList []*records.Record\n\tresult := tx.Where(\"account_id = ? AND zone_id = ?\", accountID, zoneID).Find(&recordsList)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get zone dns records from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get zone dns records from store\")\n\t}\n\n\treturn recordsList, nil\n}\n\nfunc (s *SqlStore) GetZoneDNSRecordsByName(ctx context.Context, lockStrength LockingStrength, accountID, zoneID, name string) ([]*records.Record, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar recordsList []*records.Record\n\tresult := tx.Where(\"account_id = ? AND zone_id = ? AND name = ?\", accountID, zoneID, name).Find(&recordsList)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get zone dns records by name from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get zone dns records by name from store\")\n\t}\n\n\treturn recordsList, nil\n}\n\nfunc (s *SqlStore) DeleteZoneDNSRecords(ctx context.Context, accountID, zoneID string) error {\n\tresult := s.db.Delete(&records.Record{}, \"account_id = ? AND zone_id = ?\", accountID, zoneID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete zone dns records from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete zone dns records from store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetPeerIDByKey(ctx context.Context, lockStrength LockingStrength, key string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peerID string\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tSelect(\"id\").\n\t\tWhere(GetKeyQueryCondition(s), key).\n\t\tLimit(1).\n\t\tScan(&peerID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peer ID by key: %s\", result.Error)\n\t\treturn \"\", status.Errorf(status.Internal, \"failed to get peer ID by key\")\n\t}\n\n\treturn peerID, nil\n}\n\nfunc (s *SqlStore) CreateService(ctx context.Context, service *reverseproxy.Service) error {\n\tserviceCopy := service.Copy()\n\tif err := serviceCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn fmt.Errorf(\"encrypt service data: %w\", err)\n\t}\n\tresult := s.db.Create(serviceCopy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create service to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create service to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) UpdateService(ctx context.Context, service *reverseproxy.Service) error {\n\tserviceCopy := service.Copy()\n\tif err := serviceCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn fmt.Errorf(\"encrypt service data: %w\", err)\n\t}\n\n\t// Use a transaction to ensure atomic updates of the service and its targets\n\terr := s.db.Transaction(func(tx *gorm.DB) error {\n\t\t// Delete existing targets\n\t\tif err := tx.Where(\"service_id = ?\", serviceCopy.ID).Delete(&reverseproxy.Target{}).Error; err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Update the service and create new targets\n\t\tif err := tx.Session(&gorm.Session{FullSaveAssociations: true}).Save(serviceCopy).Error; err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update service to store: %v\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to update service to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteService(ctx context.Context, accountID, serviceID string) error {\n\tresult := s.db.Delete(&reverseproxy.Service{}, accountAndIDQueryCondition, accountID, serviceID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete service from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete service from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"service %s not found\", serviceID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteTarget(ctx context.Context, accountID string, serviceID string, targetID uint) error {\n\tresult := s.db.Delete(&reverseproxy.Target{}, \"account_id = ? AND service_id = ? AND id = ?\", accountID, serviceID, targetID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete target from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete target from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"target not found for service %s\", serviceID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteServiceTargets(ctx context.Context, accountID string, serviceID string) error {\n\tresult := s.db.Delete(&reverseproxy.Target{}, \"account_id = ? AND service_id = ?\", accountID, serviceID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete targets from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete targets from store\")\n\t}\n\n\treturn nil\n}\n\n// GetTargetsByServiceID retrieves all targets for a given service\nfunc (s *SqlStore) GetTargetsByServiceID(ctx context.Context, lockStrength LockingStrength, accountID string, serviceID string) ([]*reverseproxy.Target, error) {\n\tvar targets []*reverseproxy.Target\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\tresult := tx.Where(\"account_id = ? AND service_id = ?\", accountID, serviceID).Find(&targets)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get targets from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get targets from store\")\n\t}\n\n\treturn targets, nil\n}\n\nfunc (s *SqlStore) GetServiceByID(ctx context.Context, lockStrength LockingStrength, accountID, serviceID string) (*reverseproxy.Service, error) {\n\ttx := s.db.Preload(\"Targets\")\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar service *reverseproxy.Service\n\tresult := tx.Take(&service, accountAndIDQueryCondition, accountID, serviceID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"service %s not found\", serviceID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get service from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get service from store\")\n\t}\n\n\tif err := service.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt service data: %w\", err)\n\t}\n\n\treturn service, nil\n}\n\nfunc (s *SqlStore) GetServicesByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*reverseproxy.Service, error) {\n\ttx := s.db.Preload(\"Targets\")\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar serviceList []*reverseproxy.Service\n\tresult := tx.Find(&serviceList, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get services from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get services from store\")\n\t}\n\n\tfor _, service := range serviceList {\n\t\tif err := service.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt service data: %w\", err)\n\t\t}\n\t}\n\n\treturn serviceList, nil\n}\n\nfunc (s *SqlStore) GetServiceByDomain(ctx context.Context, accountID, domain string) (*reverseproxy.Service, error) {\n\tvar service *reverseproxy.Service\n\tresult := s.db.Preload(\"Targets\").Where(\"account_id = ? AND domain = ?\", accountID, domain).First(&service)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"service with domain %s not found\", domain)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get service by domain from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get service by domain from store\")\n\t}\n\n\tif err := service.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt service data: %w\", err)\n\t}\n\n\treturn service, nil\n}\n\nfunc (s *SqlStore) GetServices(ctx context.Context, lockStrength LockingStrength) ([]*reverseproxy.Service, error) {\n\ttx := s.db.Preload(\"Targets\")\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar serviceList []*reverseproxy.Service\n\tresult := tx.Find(&serviceList)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get services from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get services from store\")\n\t}\n\n\tfor _, service := range serviceList {\n\t\tif err := service.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt service data: %w\", err)\n\t\t}\n\t}\n\n\treturn serviceList, nil\n}\n\nfunc (s *SqlStore) GetAccountServices(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*reverseproxy.Service, error) {\n\ttx := s.db.Preload(\"Targets\")\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar serviceList []*reverseproxy.Service\n\tresult := tx.Find(&serviceList, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get services from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get services from store\")\n\t}\n\n\tfor _, service := range serviceList {\n\t\tif err := service.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt service data: %w\", err)\n\t\t}\n\t}\n\n\treturn serviceList, nil\n}\n\nfunc (s *SqlStore) GetCustomDomain(ctx context.Context, accountID string, domainID string) (*domain.Domain, error) {\n\ttx := s.db\n\n\tcustomDomain := &domain.Domain{}\n\tresult := tx.Take(&customDomain, accountAndIDQueryCondition, accountID, domainID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"custom domain %s not found\", domainID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get custom domain from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get custom domain from store\")\n\t}\n\n\treturn customDomain, nil\n}\n\nfunc (s *SqlStore) ListFreeDomains(ctx context.Context, accountID string) ([]string, error) {\n\treturn nil, nil\n}\n\nfunc (s *SqlStore) ListCustomDomains(ctx context.Context, accountID string) ([]*domain.Domain, error) {\n\ttx := s.db\n\n\tvar domains []*domain.Domain\n\tresult := tx.Find(&domains, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get reverse proxy custom domains from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get reverse proxy custom domains from store\")\n\t}\n\n\treturn domains, nil\n}\n\nfunc (s *SqlStore) CreateCustomDomain(ctx context.Context, accountID string, domainName string, targetCluster string, validated bool) (*domain.Domain, error) {\n\tnewDomain := &domain.Domain{\n\t\tID: xid.New().String(), // Generate our own ID because gorm doesn't always configure the database to handle this for us.\n\t\tDomain: domainName,\n\t\tAccountID: accountID,\n\t\tTargetCluster: targetCluster,\n\t\tType: domain.TypeCustom,\n\t\tValidated: validated,\n\t}\n\tresult := s.db.Create(newDomain)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create reverse proxy custom domain to store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to create reverse proxy custom domain to store\")\n\t}\n\n\treturn newDomain, nil\n}\n\nfunc (s *SqlStore) UpdateCustomDomain(ctx context.Context, accountID string, d *domain.Domain) (*domain.Domain, error) {\n\td.AccountID = accountID\n\tresult := s.db.Select(\"*\").Save(d)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update reverse proxy custom domain to store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to update reverse proxy custom domain to store\")\n\t}\n\n\treturn d, nil\n}\n\nfunc (s *SqlStore) DeleteCustomDomain(ctx context.Context, accountID string, domainID string) error {\n\tresult := s.db.Delete(domain.Domain{}, accountAndIDQueryCondition, accountID, domainID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete reverse proxy custom domain from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete reverse proxy custom domain from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"reverse proxy custom domain %s not found\", domainID)\n\t}\n\n\treturn nil\n}\n\n// CreateAccessLog creates a new access log entry in the database\nfunc (s *SqlStore) CreateAccessLog(ctx context.Context, logEntry *accesslogs.AccessLogEntry) error {\n\tresult := s.db.Create(logEntry)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).WithFields(log.Fields{\n\t\t\t\"service_id\": logEntry.ServiceID,\n\t\t\t\"method\": logEntry.Method,\n\t\t\t\"host\": logEntry.Host,\n\t\t\t\"path\": logEntry.Path,\n\t\t}).Errorf(\"failed to create access log entry in store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create access log entry in store\")\n\t}\n\treturn nil\n}\n\n// GetAccountAccessLogs retrieves access logs for a given account with pagination and filtering\nfunc (s *SqlStore) GetAccountAccessLogs(ctx context.Context, lockStrength LockingStrength, accountID string, filter accesslogs.AccessLogFilter) ([]*accesslogs.AccessLogEntry, int64, error) {\n\tvar logs []*accesslogs.AccessLogEntry\n\tvar totalCount int64\n\n\tbaseQuery := s.db.WithContext(ctx).\n\t\tModel(&accesslogs.AccessLogEntry{}).\n\t\tWhere(accountIDCondition, accountID)\n\n\tbaseQuery = s.applyAccessLogFilters(baseQuery, filter)\n\n\tif err := baseQuery.Count(&totalCount).Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to count access logs: %v\", err)\n\t\treturn nil, 0, status.Errorf(status.Internal, \"failed to count access logs\")\n\t}\n\n\tquery := s.db.WithContext(ctx).\n\t\tWhere(accountIDCondition, accountID)\n\n\tquery = s.applyAccessLogFilters(query, filter)\n\n\tsortColumns := filter.GetSortColumn()\n\tsortOrder := strings.ToUpper(filter.GetSortOrder())\n\n\tvar orderClauses []string\n\tfor _, col := range strings.Split(sortColumns, \",\") {\n\t\tcol = strings.TrimSpace(col)\n\t\tif col != \"\" {\n\t\t\torderClauses = append(orderClauses, col+\" \"+sortOrder)\n\t\t}\n\t}\n\torderClause := strings.Join(orderClauses, \", \")\n\n\tquery = query.\n\t\tOrder(orderClause).\n\t\tLimit(filter.GetLimit()).\n\t\tOffset(filter.GetOffset())\n\n\tif lockStrength != LockingStrengthNone {\n\t\tquery = query.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tresult := query.Find(&logs)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get access logs from store: %v\", result.Error)\n\t\treturn nil, 0, status.Errorf(status.Internal, \"failed to get access logs from store\")\n\t}\n\n\treturn logs, totalCount, nil\n}\n\n// DeleteOldAccessLogs deletes all access logs older than the specified time\nfunc (s *SqlStore) DeleteOldAccessLogs(ctx context.Context, olderThan time.Time) (int64, error) {\n\tresult := s.db.WithContext(ctx).\n\t\tWhere(\"timestamp < ?\", olderThan).\n\t\tDelete(&accesslogs.AccessLogEntry{})\n\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete old access logs: %v\", result.Error)\n\t\treturn 0, status.Errorf(status.Internal, \"failed to delete old access logs\")\n\t}\n\n\treturn result.RowsAffected, nil\n}\n\n// applyAccessLogFilters applies filter conditions to the query\nfunc (s *SqlStore) applyAccessLogFilters(query *gorm.DB, filter accesslogs.AccessLogFilter) *gorm.DB {\n\tif filter.Search != nil {\n\t\tsearchPattern := \"%\" + *filter.Search + \"%\"\n\t\tquery = query.Where(\n\t\t\t\"id LIKE ? OR location_connection_ip LIKE ? OR host LIKE ? OR path LIKE ? OR CONCAT(host, path) LIKE ? OR user_id IN (SELECT id FROM users WHERE email LIKE ? OR name LIKE ?)\",\n\t\t\tsearchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern,\n\t\t)\n\t}\n\n\tif filter.SourceIP != nil {\n\t\tquery = query.Where(\"location_connection_ip = ?\", *filter.SourceIP)\n\t}\n\n\tif filter.Host != nil {\n\t\tquery = query.Where(\"host = ?\", *filter.Host)\n\t}\n\n\tif filter.Path != nil {\n\t\t// Support LIKE pattern for path filtering\n\t\tquery = query.Where(\"path LIKE ?\", \"%\"+*filter.Path+\"%\")\n\t}\n\n\tif filter.UserID != nil {\n\t\tquery = query.Where(\"user_id = ?\", *filter.UserID)\n\t}\n\n\tif filter.Method != nil {\n\t\tquery = query.Where(\"method = ?\", *filter.Method)\n\t}\n\n\tif filter.Status != nil {\n\t\tswitch *filter.Status {\n\t\tcase \"success\":\n\t\t\tquery = query.Where(\"status_code >= ? AND status_code < ?\", 200, 400)\n\t\tcase \"failed\":\n\t\t\tquery = query.Where(\"status_code < ? OR status_code >= ?\", 200, 400)\n\t\t}\n\t}\n\n\tif filter.StatusCode != nil {\n\t\tquery = query.Where(\"status_code = ?\", *filter.StatusCode)\n\t}\n\n\tif filter.StartDate != nil {\n\t\tquery = query.Where(\"timestamp >= ?\", *filter.StartDate)\n\t}\n\n\tif filter.EndDate != nil {\n\t\tquery = query.Where(\"timestamp <= ?\", *filter.EndDate)\n\t}\n\n\treturn query\n}\n\nfunc (s *SqlStore) GetServiceTargetByTargetID(ctx context.Context, lockStrength LockingStrength, accountID string, targetID string) (*reverseproxy.Target, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar target *reverseproxy.Target\n\tresult := tx.Take(&target, \"account_id = ? AND target_id = ?\", accountID, targetID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"service target with ID %s not found\", targetID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get service target from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get service target from store\")\n\t}\n\n\treturn target, nil\n}","originalFile":"package store\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n\t\"github.com/rs/xid\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gorm.io/driver/mysql\"\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/driver/sqlite\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n\t\"gorm.io/gorm/logger\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/management/internals/modules/reverseproxy\"\n\t\"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs\"\n\t\"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain\"\n\t\"github.com/netbirdio/netbird/management/internals/modules/zones\"\n\t\"github.com/netbirdio/netbird/management/internals/modules/zones/records\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/telemetry\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/management/server/util\"\n\t\"github.com/netbirdio/netbird/route\"\n\t\"github.com/netbirdio/netbird/shared/management/status\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n)\n\nconst (\n\tstoreSqliteFileName = \"store.db\"\n\tidQueryCondition = \"id = ?\"\n\tkeyQueryCondition = \"key = ?\"\n\tmysqlKeyQueryCondition = \"`key` = ?\"\n\taccountAndIDQueryCondition = \"account_id = ? and id = ?\"\n\taccountAndPeerIDQueryCondition = \"account_id = ? and peer_id = ?\"\n\taccountAndIDsQueryCondition = \"account_id = ? AND id IN ?\"\n\taccountIDCondition = \"account_id = ?\"\n\tpeerNotFoundFMT = \"peer %s not found\"\n\n\tpgMaxConnections = 30\n\tpgMinConnections = 1\n\tpgMaxConnLifetime = 60 * time.Minute\n\tpgHealthCheckPeriod = 1 * time.Minute\n)\n\n// SqlStore represents an account storage backed by a Sql DB persisted to disk\ntype SqlStore struct {\n\tdb *gorm.DB\n\tglobalAccountLock sync.Mutex\n\tmetrics telemetry.AppMetrics\n\tinstallationPK int\n\tstoreEngine types.Engine\n\tpool *pgxpool.Pool\n\tfieldEncrypt *crypt.FieldEncrypt\n\ttransactionTimeout time.Duration\n}\n\ntype installation struct {\n\tID uint `gorm:\"primaryKey\"`\n\tInstallationIDValue string\n}\n\ntype migrationFunc func(*gorm.DB) error\n\n// NewSqlStore creates a new SqlStore instance.\nfunc NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tsql, err := db.DB()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconns, err := strconv.Atoi(os.Getenv(\"NB_SQL_MAX_OPEN_CONNS\"))\n\tif err != nil {\n\t\tconns = runtime.NumCPU()\n\t}\n\n\ttransactionTimeout := 5 * time.Minute\n\tif v := os.Getenv(\"NB_STORE_TRANSACTION_TIMEOUT\"); v != \"\" {\n\t\tif parsed, err := time.ParseDuration(v); err == nil {\n\t\t\ttransactionTimeout = parsed\n\t\t}\n\t}\n\tlog.WithContext(ctx).Infof(\"Setting transaction timeout to %v\", transactionTimeout)\n\n\tif storeEngine == types.SqliteStoreEngine {\n\t\tif err == nil {\n\t\t\tlog.WithContext(ctx).Warnf(\"setting NB_SQL_MAX_OPEN_CONNS is not supported for sqlite, using default value 1\")\n\t\t}\n\t\tconns = 1\n\t}\n\n\tsql.SetMaxOpenConns(conns)\n\tsql.SetMaxIdleConns(conns)\n\tsql.SetConnMaxLifetime(time.Hour)\n\tsql.SetConnMaxIdleTime(3 * time.Minute)\n\n\tlog.WithContext(ctx).Infof(\"Set max open db connections to %d, max idle to %d, max lifetime to %v, max idle time to %v\",\n\t\tconns, conns, time.Hour, 3*time.Minute)\n\n\tif skipMigration {\n\t\tlog.WithContext(ctx).Infof(\"skipping migration\")\n\t\treturn &SqlStore{db: db, storeEngine: storeEngine, metrics: metrics, installationPK: 1, transactionTimeout: transactionTimeout}, nil\n\t}\n\n\tif err := migratePreAuto(ctx, db); err != nil {\n\t\treturn nil, fmt.Errorf(\"migratePreAuto: %w\", err)\n\t}\n\terr = db.AutoMigrate(\n\t\t&types.SetupKey{}, &nbpeer.Peer{}, &types.User{}, &types.PersonalAccessToken{}, &types.ProxyAccessToken{},\n\t\t&types.Group{}, &types.GroupPeer{},\n\t\t&types.Account{}, &types.Policy{}, &types.PolicyRule{}, &route.Route{}, &nbdns.NameServerGroup{},\n\t\t&installation{}, &types.ExtraSettings{}, &posture.Checks{}, &nbpeer.NetworkAddress{},\n\t\t&networkTypes.Network{}, &routerTypes.NetworkRouter{}, &resourceTypes.NetworkResource{}, &types.AccountOnboarding{},\n\t\t&types.Job{}, &zones.Zone{}, &records.Record{}, &types.UserInviteRecord{}, &reverseproxy.Service{}, &reverseproxy.Target{}, &domain.Domain{},\n\t\t&accesslogs.AccessLogEntry{},\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"auto migratePreAuto: %w\", err)\n\t}\n\tif err := migratePostAuto(ctx, db); err != nil {\n\t\treturn nil, fmt.Errorf(\"migratePostAuto: %w\", err)\n\t}\n\n\treturn &SqlStore{db: db, storeEngine: storeEngine, metrics: metrics, installationPK: 1, transactionTimeout: transactionTimeout}, nil\n}\n\nfunc GetKeyQueryCondition(s *SqlStore) string {\n\tif s.storeEngine == types.MysqlStoreEngine {\n\t\treturn mysqlKeyQueryCondition\n\t}\n\treturn keyQueryCondition\n}\n\n// SaveJob persists a job in DB\nfunc (s *SqlStore) CreatePeerJob(ctx context.Context, job *types.Job) error {\n\tresult := s.db.Create(job)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create job in store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create job in store\")\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) CompletePeerJob(ctx context.Context, job *types.Job) error {\n\tresult := s.db.\n\t\tModel(&types.Job{}).\n\t\tWhere(idQueryCondition, job.ID).\n\t\tUpdates(job)\n\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update job in store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to update job in store\")\n\t}\n\treturn nil\n}\n\n// job was pending for too long and has been cancelled\nfunc (s *SqlStore) MarkPendingJobsAsFailed(ctx context.Context, accountID, peerID, jobID, reason string) error {\n\tnow := time.Now().UTC()\n\tresult := s.db.\n\t\tModel(&types.Job{}).\n\t\tWhere(accountAndPeerIDQueryCondition+\" AND id = ?\"+\" AND status = ?\", accountID, peerID, jobID, types.JobStatusPending).\n\t\tUpdates(types.Job{\n\t\t\tStatus: types.JobStatusFailed,\n\t\t\tFailedReason: reason,\n\t\t\tCompletedAt: &now,\n\t\t})\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to mark pending jobs as Failed job in store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to mark pending job as Failed in store\")\n\t}\n\treturn nil\n}\n\n// job was pending for too long and has been cancelled\nfunc (s *SqlStore) MarkAllPendingJobsAsFailed(ctx context.Context, accountID, peerID, reason string) error {\n\tnow := time.Now().UTC()\n\tresult := s.db.\n\t\tModel(&types.Job{}).\n\t\tWhere(accountAndPeerIDQueryCondition+\" AND status = ?\", accountID, peerID, types.JobStatusPending).\n\t\tUpdates(types.Job{\n\t\t\tStatus: types.JobStatusFailed,\n\t\t\tFailedReason: reason,\n\t\t\tCompletedAt: &now,\n\t\t})\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to mark pending jobs as Failed job in store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to mark pending job as Failed in store\")\n\t}\n\treturn nil\n}\n\n// GetJobByID fetches job by ID\nfunc (s *SqlStore) GetPeerJobByID(ctx context.Context, accountID, jobID string) (*types.Job, error) {\n\tvar job types.Job\n\terr := s.db.\n\t\tWhere(accountAndIDQueryCondition, accountID, jobID).\n\t\tFirst(&job).Error\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\treturn nil, status.Errorf(status.NotFound, \"job %s not found\", jobID)\n\t}\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to fetch job from store: %s\", err)\n\t\treturn nil, err\n\t}\n\treturn &job, nil\n}\n\n// get all jobs\nfunc (s *SqlStore) GetPeerJobs(ctx context.Context, accountID, peerID string) ([]*types.Job, error) {\n\tvar jobs []*types.Job\n\terr := s.db.\n\t\tWhere(accountAndPeerIDQueryCondition, accountID, peerID).\n\t\tOrder(\"created_at DESC\").\n\t\tFind(&jobs).Error\n\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to fetch jobs from store: %s\", err)\n\t\treturn nil, err\n\t}\n\n\treturn jobs, nil\n}\n\n// AcquireGlobalLock acquires global lock across all the accounts and returns a function that releases the lock\nfunc (s *SqlStore) AcquireGlobalLock(ctx context.Context) (unlock func()) {\n\tlog.WithContext(ctx).Tracef(\"acquiring global lock\")\n\tstart := time.Now()\n\ts.globalAccountLock.Lock()\n\n\tunlock = func() {\n\t\ts.globalAccountLock.Unlock()\n\t\tlog.WithContext(ctx).Tracef(\"released global lock in %v\", time.Since(start))\n\t}\n\n\ttook := time.Since(start)\n\tlog.WithContext(ctx).Tracef(\"took %v to acquire global lock\", took)\n\tif s.metrics != nil {\n\t\ts.metrics.StoreMetrics().CountGlobalLockAcquisitionDuration(took)\n\t}\n\n\treturn unlock\n}\n\n// Deprecated: Full account operations are no longer supported\nfunc (s *SqlStore) SaveAccount(ctx context.Context, account *types.Account) error {\n\tstart := time.Now()\n\tdefer func() {\n\t\telapsed := time.Since(start)\n\t\tif elapsed > 1*time.Second {\n\t\t\tlog.WithContext(ctx).Tracef(\"SaveAccount for account %s exceeded 1s, took: %v\", account.Id, elapsed)\n\t\t}\n\t}()\n\n\t// todo: remove this check after the issue is resolved\n\ts.checkAccountDomainBeforeSave(ctx, account.Id, account.Domain)\n\n\tgenerateAccountSQLTypes(account)\n\n\t// Encrypt sensitive user data before saving\n\tfor i := range account.UsersG {\n\t\tif err := account.UsersG[i].EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn fmt.Errorf(\"encrypt user: %w\", err)\n\t\t}\n\t}\n\n\tfor _, group := range account.GroupsG {\n\t\tgroup.StoreGroupPeers()\n\t}\n\n\terr := s.transaction(func(tx *gorm.DB) error {\n\t\tresult := tx.Select(clause.Associations).Delete(account.Policies, \"account_id = ?\", account.Id)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\tresult = tx.Select(clause.Associations).Delete(account.UsersG, \"account_id = ?\", account.Id)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\tresult = tx.Select(clause.Associations).Delete(account)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\tresult = tx.\n\t\t\tSession(&gorm.Session{FullSaveAssociations: true}).\n\t\t\tClauses(clause.OnConflict{UpdateAll: true}).\n\t\t\tCreate(account)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\t\treturn nil\n\t})\n\n\ttook := time.Since(start)\n\tif s.metrics != nil {\n\t\ts.metrics.StoreMetrics().CountPersistenceDuration(took)\n\t}\n\tlog.WithContext(ctx).Debugf(\"took %d ms to persist an account to the store\", took.Milliseconds())\n\n\treturn err\n}\n\n// generateAccountSQLTypes generates the GORM compatible types for the account\nfunc generateAccountSQLTypes(account *types.Account) {\n\tfor _, key := range account.SetupKeys {\n\t\taccount.SetupKeysG = append(account.SetupKeysG, *key)\n\t}\n\n\tif len(account.SetupKeys) != len(account.SetupKeysG) {\n\t\tlog.Warnf(\"SetupKeysG length mismatch for account %s\", account.Id)\n\t}\n\n\tfor id, peer := range account.Peers {\n\t\tpeer.ID = id\n\t\taccount.PeersG = append(account.PeersG, *peer)\n\t}\n\n\tfor id, user := range account.Users {\n\t\tuser.Id = id\n\t\tfor id, pat := range user.PATs {\n\t\t\tpat.ID = id\n\t\t\tuser.PATsG = append(user.PATsG, *pat)\n\t\t}\n\t\taccount.UsersG = append(account.UsersG, *user)\n\t}\n\n\tfor id, group := range account.Groups {\n\t\tgroup.ID = id\n\t\tgroup.AccountID = account.Id\n\t\taccount.GroupsG = append(account.GroupsG, group)\n\t}\n\n\tfor id, route := range account.Routes {\n\t\troute.ID = id\n\t\taccount.RoutesG = append(account.RoutesG, *route)\n\t}\n\n\tfor id, ns := range account.NameServerGroups {\n\t\tns.ID = id\n\t\taccount.NameServerGroupsG = append(account.NameServerGroupsG, *ns)\n\t}\n}\n\n// checkAccountDomainBeforeSave temporary method to troubleshoot an issue with domains getting blank\nfunc (s *SqlStore) checkAccountDomainBeforeSave(ctx context.Context, accountID, newDomain string) {\n\tvar acc types.Account\n\tvar domain string\n\tresult := s.db.Model(&acc).Select(\"domain\").Where(idQueryCondition, accountID).Take(&domain)\n\tif result.Error != nil {\n\t\tif !errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\tlog.WithContext(ctx).Errorf(\"error when getting account %s from the store to check domain: %s\", accountID, result.Error)\n\t\t}\n\t\treturn\n\t}\n\tif domain != \"\" && newDomain == \"\" {\n\t\tlog.WithContext(ctx).Warnf(\"saving an account with empty domain when there was a domain set. Previous domain %s, Account ID: %s, Trace: %s\", domain, accountID, debug.Stack())\n\t}\n}\n\nfunc (s *SqlStore) DeleteAccount(ctx context.Context, account *types.Account) error {\n\tstart := time.Now()\n\n\terr := s.transaction(func(tx *gorm.DB) error {\n\t\tresult := tx.Select(clause.Associations).Delete(account.Policies, \"account_id = ?\", account.Id)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\tresult = tx.Select(clause.Associations).Delete(account.UsersG, \"account_id = ?\", account.Id)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\tresult = tx.Select(clause.Associations).Delete(account)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\treturn nil\n\t})\n\n\ttook := time.Since(start)\n\tif s.metrics != nil {\n\t\ts.metrics.StoreMetrics().CountPersistenceDuration(took)\n\t}\n\tlog.WithContext(ctx).Tracef(\"took %d ms to delete an account to the store\", took.Milliseconds())\n\n\treturn err\n}\n\nfunc (s *SqlStore) SaveInstallationID(_ context.Context, ID string) error {\n\tinstallation := installation{InstallationIDValue: ID}\n\tinstallation.ID = uint(s.installationPK)\n\n\treturn s.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&installation).Error\n}\n\nfunc (s *SqlStore) GetInstallationID() string {\n\tvar installation installation\n\n\tif result := s.db.Take(&installation, idQueryCondition, s.installationPK); result.Error != nil {\n\t\treturn \"\"\n\t}\n\n\treturn installation.InstallationIDValue\n}\n\nfunc (s *SqlStore) SavePeer(ctx context.Context, accountID string, peer *nbpeer.Peer) error {\n\t// To maintain data integrity, we create a copy of the peer's to prevent unintended updates to other fields.\n\tpeerCopy := peer.Copy()\n\tpeerCopy.AccountID = accountID\n\n\terr := s.transaction(func(tx *gorm.DB) error {\n\t\t// check if peer exists before saving\n\t\tvar peerID string\n\t\tresult := tx.Model(&nbpeer.Peer{}).Select(\"id\").Take(&peerID, accountAndIDQueryCondition, accountID, peer.ID)\n\t\tif result.Error != nil {\n\t\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\t\treturn status.Errorf(status.NotFound, peerNotFoundFMT, peer.ID)\n\t\t\t}\n\t\t\treturn result.Error\n\t\t}\n\n\t\tif peerID == \"\" {\n\t\t\treturn status.Errorf(status.NotFound, peerNotFoundFMT, peer.ID)\n\t\t}\n\n\t\tresult = tx.Model(&nbpeer.Peer{}).Where(accountAndIDQueryCondition, accountID, peer.ID).Save(peerCopy)\n\t\tif result.Error != nil {\n\t\t\treturn status.Errorf(status.Internal, \"failed to save peer to store: %v\", result.Error)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) UpdateAccountDomainAttributes(ctx context.Context, accountID string, domain string, category string, isPrimaryDomain bool) error {\n\taccountCopy := types.Account{\n\t\tDomain: domain,\n\t\tDomainCategory: category,\n\t\tIsDomainPrimaryAccount: isPrimaryDomain,\n\t}\n\n\tfieldsToUpdate := []string{\"domain\", \"domain_category\", \"is_domain_primary_account\"}\n\tresult := s.db.Model(&types.Account{}).\n\t\tSelect(fieldsToUpdate).\n\t\tWhere(idQueryCondition, accountID).\n\t\tUpdates(&accountCopy)\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"failed to update account domain attributes to store: %v\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"account %s\", accountID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) SavePeerStatus(ctx context.Context, accountID, peerID string, peerStatus nbpeer.PeerStatus) error {\n\tvar peerCopy nbpeer.Peer\n\tpeerCopy.Status = &peerStatus\n\n\tfieldsToUpdate := []string{\n\t\t\"peer_status_last_seen\", \"peer_status_connected\",\n\t\t\"peer_status_login_expired\", \"peer_status_required_approval\",\n\t}\n\tresult := s.db.Model(&nbpeer.Peer{}).\n\t\tSelect(fieldsToUpdate).\n\t\tWhere(accountAndIDQueryCondition, accountID, peerID).\n\t\tUpdates(&peerCopy)\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"failed to save peer status to store: %v\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, peerNotFoundFMT, peerID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) SavePeerLocation(ctx context.Context, accountID string, peerWithLocation *nbpeer.Peer) error {\n\t// To maintain data integrity, we create a copy of the peer's location to prevent unintended updates to other fields.\n\tvar peerCopy nbpeer.Peer\n\t// Since the location field has been migrated to JSON serialization,\n\t// updating the struct ensures the correct data format is inserted into the database.\n\tpeerCopy.Location = peerWithLocation.Location\n\n\tresult := s.db.Model(&nbpeer.Peer{}).\n\t\tWhere(accountAndIDQueryCondition, accountID, peerWithLocation.ID).\n\t\tUpdates(peerCopy)\n\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"failed to save peer locations to store: %v\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, peerNotFoundFMT, peerWithLocation.ID)\n\t}\n\n\treturn nil\n}\n\n// ApproveAccountPeers marks all peers that currently require approval in the given account as approved.\nfunc (s *SqlStore) ApproveAccountPeers(ctx context.Context, accountID string) (int, error) {\n\tresult := s.db.Model(&nbpeer.Peer{}).\n\t\tWhere(\"account_id = ? AND peer_status_requires_approval = ?\", accountID, true).\n\t\tUpdate(\"peer_status_requires_approval\", false)\n\tif result.Error != nil {\n\t\treturn 0, status.Errorf(status.Internal, \"failed to approve pending account peers: %v\", result.Error)\n\t}\n\n\treturn int(result.RowsAffected), nil\n}\n\n// SaveUsers saves the given list of users to the database.\nfunc (s *SqlStore) SaveUsers(ctx context.Context, users []*types.User) error {\n\tif len(users) == 0 {\n\t\treturn nil\n\t}\n\n\tusersCopy := make([]*types.User, len(users))\n\tfor i, user := range users {\n\t\tuserCopy := user.Copy()\n\t\tuserCopy.Email = user.Email\n\t\tuserCopy.Name = user.Name\n\t\tif err := userCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn fmt.Errorf(\"encrypt user: %w\", err)\n\t\t}\n\t\tusersCopy[i] = userCopy\n\t}\n\n\tresult := s.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&usersCopy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save users to store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save users to store\")\n\t}\n\treturn nil\n}\n\n// SaveUser saves the given user to the database.\nfunc (s *SqlStore) SaveUser(ctx context.Context, user *types.User) error {\n\tuserCopy := user.Copy()\n\tuserCopy.Email = user.Email\n\tuserCopy.Name = user.Name\n\n\tif err := userCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn fmt.Errorf(\"encrypt user: %w\", err)\n\t}\n\n\tresult := s.db.Save(userCopy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save user to store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save user to store\")\n\t}\n\treturn nil\n}\n\n// CreateGroups creates the given list of groups to the database.\nfunc (s *SqlStore) CreateGroups(ctx context.Context, accountID string, groups []*types.Group) error {\n\tif len(groups) == 0 {\n\t\treturn nil\n\t}\n\n\treturn s.db.Transaction(func(tx *gorm.DB) error {\n\t\tresult := tx.\n\t\t\tClauses(\n\t\t\t\tclause.OnConflict{\n\t\t\t\t\tWhere: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: \"groups.account_id\", Value: accountID}}},\n\t\t\t\t\tUpdateAll: true,\n\t\t\t\t},\n\t\t\t).\n\t\t\tOmit(clause.Associations).\n\t\t\tCreate(&groups)\n\t\tif result.Error != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"failed to save groups to store: %v\", result.Error)\n\t\t\treturn status.Errorf(status.Internal, \"failed to save groups to store\")\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\n// UpdateGroups updates the given list of groups to the database.\nfunc (s *SqlStore) UpdateGroups(ctx context.Context, accountID string, groups []*types.Group) error {\n\tif len(groups) == 0 {\n\t\treturn nil\n\t}\n\n\treturn s.db.Transaction(func(tx *gorm.DB) error {\n\t\tresult := tx.\n\t\t\tClauses(\n\t\t\t\tclause.OnConflict{\n\t\t\t\t\tWhere: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: \"groups.account_id\", Value: accountID}}},\n\t\t\t\t\tUpdateAll: true,\n\t\t\t\t},\n\t\t\t).\n\t\t\tOmit(clause.Associations).\n\t\t\tCreate(&groups)\n\t\tif result.Error != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"failed to save groups to store: %v\", result.Error)\n\t\t\treturn status.Errorf(status.Internal, \"failed to save groups to store\")\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\n// DeleteHashedPAT2TokenIDIndex is noop in SqlStore\nfunc (s *SqlStore) DeleteHashedPAT2TokenIDIndex(hashedToken string) error {\n\treturn nil\n}\n\n// DeleteTokenID2UserIDIndex is noop in SqlStore\nfunc (s *SqlStore) DeleteTokenID2UserIDIndex(tokenID string) error {\n\treturn nil\n}\n\nfunc (s *SqlStore) GetAccountByPrivateDomain(ctx context.Context, domain string) (*types.Account, error) {\n\taccountID, err := s.GetAccountIDByPrivateDomain(ctx, LockingStrengthNone, domain)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// TODO: rework to not call GetAccount\n\treturn s.GetAccount(ctx, accountID)\n}\n\nfunc (s *SqlStore) GetAccountIDByPrivateDomain(ctx context.Context, lockStrength LockingStrength, domain string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountID string\n\tresult := tx.Model(&types.Account{}).Select(\"id\").\n\t\tWhere(\"domain = ? and is_domain_primary_account = ? and domain_category = ?\",\n\t\t\tstrings.ToLower(domain), true, types.PrivateCategory,\n\t\t).Take(&accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: provided domain is not registered or is not private\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"error when getting account from the store: %s\", result.Error)\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn accountID, nil\n}\n\nfunc (s *SqlStore) GetAccountBySetupKey(ctx context.Context, setupKey string) (*types.Account, error) {\n\tvar key types.SetupKey\n\tresult := s.db.Select(\"account_id\").Take(&key, GetKeyQueryCondition(s), setupKey)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewSetupKeyNotFoundError(setupKey)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get account by setup key from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get account by setup key from store\")\n\t}\n\n\tif key.AccountID == \"\" {\n\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn s.GetAccount(ctx, key.AccountID)\n}\n\nfunc (s *SqlStore) GetTokenIDByHashedToken(ctx context.Context, hashedToken string) (string, error) {\n\tvar token types.PersonalAccessToken\n\tresult := s.db.Take(&token, \"hashed_token = ?\", hashedToken)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"error when getting token from the store: %s\", result.Error)\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn token.ID, nil\n}\n\nfunc (s *SqlStore) GetUserByPATID(ctx context.Context, lockStrength LockingStrength, patID string) (*types.User, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar user types.User\n\tresult := tx.\n\t\tJoins(\"JOIN personal_access_tokens ON personal_access_tokens.user_id = users.id\").\n\t\tWhere(\"personal_access_tokens.id = ?\", patID).Take(&user)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPATNotFoundError(patID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get token user from the store: %s\", result.Error)\n\t\treturn nil, status.NewGetUserFromStoreError()\n\t}\n\n\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t}\n\n\treturn &user, nil\n}\n\nfunc (s *SqlStore) GetUserByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (*types.User, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar user types.User\n\tresult := tx.Take(&user, idQueryCondition, userID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewUserNotFoundError(userID)\n\t\t}\n\t\treturn nil, status.NewGetUserFromStoreError()\n\t}\n\n\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t}\n\n\treturn &user, nil\n}\n\nfunc (s *SqlStore) DeleteUser(ctx context.Context, accountID, userID string) error {\n\terr := s.transaction(func(tx *gorm.DB) error {\n\t\tresult := tx.Delete(&types.PersonalAccessToken{}, \"user_id = ?\", userID)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\treturn tx.Delete(&types.User{}, accountAndIDQueryCondition, accountID, userID).Error\n\t})\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete user from the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to delete user from store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetAccountUsers(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.User, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar users []*types.User\n\tresult := tx.Find(&users, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"accountID not found: index lookup failed\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"error when getting users from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting users from store\")\n\t}\n\n\tfor _, user := range users {\n\t\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t\t}\n\t}\n\n\treturn users, nil\n}\n\nfunc (s *SqlStore) GetAccountOwner(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.User, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar user types.User\n\tresult := tx.Take(&user, \"account_id = ? AND role = ?\", accountID, types.UserRoleOwner)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"account owner not found: index lookup failed\")\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get account owner from the store\")\n\t}\n\n\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t}\n\n\treturn &user, nil\n}\n\n// SaveUserInvite saves a user invite to the database\nfunc (s *SqlStore) SaveUserInvite(ctx context.Context, invite *types.UserInviteRecord) error {\n\tinviteCopy := invite.Copy()\n\tif err := inviteCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn fmt.Errorf(\"encrypt invite: %w\", err)\n\t}\n\n\tresult := s.db.Save(inviteCopy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save user invite to store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save user invite to store\")\n\t}\n\treturn nil\n}\n\n// GetUserInviteByID retrieves a user invite by its ID and account ID\nfunc (s *SqlStore) GetUserInviteByID(ctx context.Context, lockStrength LockingStrength, accountID, inviteID string) (*types.UserInviteRecord, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar invite types.UserInviteRecord\n\tresult := tx.Where(\"account_id = ?\", accountID).Take(&invite, idQueryCondition, inviteID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"user invite not found\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get user invite from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get user invite from store\")\n\t}\n\n\tif err := invite.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt invite: %w\", err)\n\t}\n\n\treturn &invite, nil\n}\n\n// GetUserInviteByHashedToken retrieves a user invite by its hashed token\nfunc (s *SqlStore) GetUserInviteByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken string) (*types.UserInviteRecord, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar invite types.UserInviteRecord\n\tresult := tx.Take(&invite, \"hashed_token = ?\", hashedToken)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"user invite not found\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get user invite from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get user invite from store\")\n\t}\n\n\tif err := invite.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt invite: %w\", err)\n\t}\n\n\treturn &invite, nil\n}\n\n// GetUserInviteByEmail retrieves a user invite by account ID and email.\n// Since email is encrypted with random IVs, we fetch all invites for the account\n// and compare emails in memory after decryption.\nfunc (s *SqlStore) GetUserInviteByEmail(ctx context.Context, lockStrength LockingStrength, accountID, email string) (*types.UserInviteRecord, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar invites []*types.UserInviteRecord\n\tresult := tx.Find(&invites, \"account_id = ?\", accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get user invites from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get user invites from store\")\n\t}\n\n\tfor _, invite := range invites {\n\t\tif err := invite.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt invite: %w\", err)\n\t\t}\n\t\tif strings.EqualFold(invite.Email, email) {\n\t\t\treturn invite, nil\n\t\t}\n\t}\n\n\treturn nil, status.Errorf(status.NotFound, \"user invite not found for email\")\n}\n\n// GetAccountUserInvites retrieves all user invites for an account\nfunc (s *SqlStore) GetAccountUserInvites(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.UserInviteRecord, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar invites []*types.UserInviteRecord\n\tresult := tx.Find(&invites, \"account_id = ?\", accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get user invites from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get user invites from store\")\n\t}\n\n\tfor _, invite := range invites {\n\t\tif err := invite.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt invite: %w\", err)\n\t\t}\n\t}\n\n\treturn invites, nil\n}\n\n// DeleteUserInvite deletes a user invite by its ID\nfunc (s *SqlStore) DeleteUserInvite(ctx context.Context, inviteID string) error {\n\tresult := s.db.Delete(&types.UserInviteRecord{}, idQueryCondition, inviteID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete user invite from store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete user invite from store\")\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) GetAccountGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.Group, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar groups []*types.Group\n\tresult := tx.Preload(clause.Associations).Find(&groups, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"accountID not found: index lookup failed\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get account groups from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get account groups from the store\")\n\t}\n\n\tfor _, g := range groups {\n\t\tg.LoadGroupPeers()\n\t}\n\n\treturn groups, nil\n}\n\nfunc (s *SqlStore) GetResourceGroups(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) ([]*types.Group, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar groups []*types.Group\n\n\tlikePattern := `%\"ID\":\"` + resourceID + `\"%`\n\n\tresult := tx.\n\t\tPreload(clause.Associations).\n\t\tWhere(\"resources LIKE ?\", likePattern).\n\t\tFind(&groups)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, result.Error\n\t}\n\n\tfor _, g := range groups {\n\t\tg.LoadGroupPeers()\n\t}\n\n\treturn groups, nil\n}\n\nfunc (s *SqlStore) GetAccountsCounter(ctx context.Context) (int64, error) {\n\tvar count int64\n\tresult := s.db.Model(&types.Account{}).Count(&count)\n\tif result.Error != nil {\n\t\treturn 0, fmt.Errorf(\"failed to get all accounts counter: %w\", result.Error)\n\t}\n\n\treturn count, nil\n}\n\n// GetCustomDomainsCounts returns the total and validated custom domain counts.\nfunc (s *SqlStore) GetCustomDomainsCounts(ctx context.Context) (int64, int64, error) {\n\tvar total, validated int64\n\tif err := s.db.WithContext(ctx).Model(&domain.Domain{}).Count(&total).Error; err != nil {\n\t\treturn 0, 0, err\n\t}\n\tif err := s.db.WithContext(ctx).Model(&domain.Domain{}).Where(\"validated = ?\", true).Count(&validated).Error; err != nil {\n\t\treturn 0, 0, err\n\t}\n\treturn total, validated, nil\n}\n\nfunc (s *SqlStore) GetAllAccounts(ctx context.Context) (all []*types.Account) {\n\tvar accounts []types.Account\n\tresult := s.db.Find(&accounts)\n\tif result.Error != nil {\n\t\treturn all\n\t}\n\n\tfor _, account := range accounts {\n\t\tif acc, err := s.GetAccount(ctx, account.Id); err == nil {\n\t\t\tall = append(all, acc)\n\t\t}\n\t}\n\n\treturn all\n}\n\nfunc (s *SqlStore) GetAccountMeta(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.AccountMeta, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountMeta types.AccountMeta\n\tresult := tx.Model(&types.Account{}).\n\t\tTake(&accountMeta, idQueryCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"error when getting account meta %s from the store: %s\", accountID, result.Error)\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn &accountMeta, nil\n}\n\n// GetAccountOnboarding retrieves the onboarding information for a specific account.\nfunc (s *SqlStore) GetAccountOnboarding(ctx context.Context, accountID string) (*types.AccountOnboarding, error) {\n\tvar accountOnboarding types.AccountOnboarding\n\tresult := s.db.Model(&accountOnboarding).Take(&accountOnboarding, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewAccountOnboardingNotFoundError(accountID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"error when getting account onboarding %s from the store: %s\", accountID, result.Error)\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn &accountOnboarding, nil\n}\n\n// SaveAccountOnboarding updates the onboarding information for a specific account.\nfunc (s *SqlStore) SaveAccountOnboarding(ctx context.Context, onboarding *types.AccountOnboarding) error {\n\tresult := s.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(onboarding)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"error when saving account onboarding %s in the store: %s\", onboarding.AccountID, result.Error)\n\t\treturn status.Errorf(status.Internal, \"error when saving account onboarding %s in the store: %s\", onboarding.AccountID, result.Error)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetAccount(ctx context.Context, accountID string) (*types.Account, error) {\n\tif s.pool != nil {\n\t\treturn s.getAccountPgx(ctx, accountID)\n\t}\n\treturn s.getAccountGorm(ctx, accountID)\n}\n\nfunc (s *SqlStore) getAccountGorm(ctx context.Context, accountID string) (*types.Account, error) {\n\tstart := time.Now()\n\tdefer func() {\n\t\telapsed := time.Since(start)\n\t\tif elapsed > 1*time.Second {\n\t\t\tlog.WithContext(ctx).Tracef(\"GetAccount for account %s exceeded 1s, took: %v\", accountID, elapsed)\n\t\t}\n\t}()\n\n\tvar account types.Account\n\tresult := s.db.Model(&account).\n\t\tPreload(\"UsersG.PATsG\"). // have to be specified as this is nested reference\n\t\tPreload(\"Policies.Rules\").\n\t\tPreload(\"SetupKeysG\").\n\t\tPreload(\"PeersG\").\n\t\tPreload(\"UsersG\").\n\t\tPreload(\"GroupsG.GroupPeers\").\n\t\tPreload(\"RoutesG\").\n\t\tPreload(\"NameServerGroupsG\").\n\t\tPreload(\"PostureChecks\").\n\t\tPreload(\"Networks\").\n\t\tPreload(\"NetworkRouters\").\n\t\tPreload(\"NetworkResources\").\n\t\tPreload(\"Onboarding\").\n\t\tPreload(\"Services.Targets\").\n\t\tTake(&account, idQueryCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"error when getting account %s from the store: %s\", accountID, result.Error)\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\taccount.SetupKeys = make(map[string]*types.SetupKey, len(account.SetupKeysG))\n\tfor _, key := range account.SetupKeysG {\n\t\tif key.UpdatedAt.IsZero() {\n\t\t\tkey.UpdatedAt = key.CreatedAt\n\t\t}\n\t\tif key.AutoGroups == nil {\n\t\t\tkey.AutoGroups = []string{}\n\t\t}\n\t\taccount.SetupKeys[key.Key] = &key\n\t}\n\taccount.SetupKeysG = nil\n\n\taccount.Peers = make(map[string]*nbpeer.Peer, len(account.PeersG))\n\tfor _, peer := range account.PeersG {\n\t\taccount.Peers[peer.ID] = &peer\n\t}\n\taccount.PeersG = nil\n\taccount.Users = make(map[string]*types.User, len(account.UsersG))\n\tfor _, user := range account.UsersG {\n\t\tuser.PATs = make(map[string]*types.PersonalAccessToken, len(user.PATs))\n\t\tfor _, pat := range user.PATsG {\n\t\t\tpat.UserID = \"\"\n\t\t\tuser.PATs[pat.ID] = &pat\n\t\t}\n\t\tif user.AutoGroups == nil {\n\t\t\tuser.AutoGroups = []string{}\n\t\t}\n\t\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t\t}\n\t\taccount.Users[user.Id] = &user\n\t\tuser.PATsG = nil\n\t}\n\taccount.UsersG = nil\n\taccount.Groups = make(map[string]*types.Group, len(account.GroupsG))\n\tfor _, group := range account.GroupsG {\n\t\tgroup.Peers = make([]string, len(group.GroupPeers))\n\t\tfor i, gp := range group.GroupPeers {\n\t\t\tgroup.Peers[i] = gp.PeerID\n\t\t}\n\t\tif group.Resources == nil {\n\t\t\tgroup.Resources = []types.Resource{}\n\t\t}\n\t\taccount.Groups[group.ID] = group\n\t}\n\taccount.GroupsG = nil\n\n\taccount.Routes = make(map[route.ID]*route.Route, len(account.RoutesG))\n\tfor _, route := range account.RoutesG {\n\t\taccount.Routes[route.ID] = &route\n\t}\n\taccount.RoutesG = nil\n\taccount.NameServerGroups = make(map[string]*nbdns.NameServerGroup, len(account.NameServerGroupsG))\n\tfor _, ns := range account.NameServerGroupsG {\n\t\tns.AccountID = \"\"\n\t\tif ns.NameServers == nil {\n\t\t\tns.NameServers = []nbdns.NameServer{}\n\t\t}\n\t\tif ns.Groups == nil {\n\t\t\tns.Groups = []string{}\n\t\t}\n\t\tif ns.Domains == nil {\n\t\t\tns.Domains = []string{}\n\t\t}\n\t\taccount.NameServerGroups[ns.ID] = &ns\n\t}\n\taccount.NameServerGroupsG = nil\n\taccount.InitOnce()\n\treturn &account, nil\n}\n\nfunc (s *SqlStore) getAccountPgx(ctx context.Context, accountID string) (*types.Account, error) {\n\taccount, err := s.getAccount(ctx, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar wg sync.WaitGroup\n\terrChan := make(chan error, 12)\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tkeys, err := s.getSetupKeys(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.SetupKeysG = keys\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tpeers, err := s.getPeers(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.PeersG = peers\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tusers, err := s.getUsers(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.UsersG = users\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tgroups, err := s.getGroups(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.GroupsG = groups\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tpolicies, err := s.getPolicies(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.Policies = policies\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\troutes, err := s.getRoutes(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.RoutesG = routes\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tnsgs, err := s.getNameServerGroups(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.NameServerGroupsG = nsgs\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tchecks, err := s.getPostureChecks(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.PostureChecks = checks\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tservices, err := s.getServices(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.Services = services\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tnetworks, err := s.getNetworks(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.Networks = networks\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\trouters, err := s.getNetworkRouters(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.NetworkRouters = routers\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tresources, err := s.getNetworkResources(ctx, accountID)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\taccount.NetworkResources = resources\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\terr := s.getAccountOnboarding(ctx, accountID, account)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t}()\n\n\twg.Wait()\n\tclose(errChan)\n\tfor e := range errChan {\n\t\tif e != nil {\n\t\t\treturn nil, e\n\t\t}\n\t}\n\n\tvar userIDs []string\n\tfor _, u := range account.UsersG {\n\t\tuserIDs = append(userIDs, u.Id)\n\t}\n\tvar policyIDs []string\n\tfor _, p := range account.Policies {\n\t\tpolicyIDs = append(policyIDs, p.ID)\n\t}\n\tvar groupIDs []string\n\tfor _, g := range account.GroupsG {\n\t\tgroupIDs = append(groupIDs, g.ID)\n\t}\n\n\twg.Add(3)\n\terrChan = make(chan error, 3)\n\n\tvar pats []types.PersonalAccessToken\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tvar err error\n\t\tpats, err = s.getPersonalAccessTokens(ctx, userIDs)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t}\n\t}()\n\n\tvar rules []*types.PolicyRule\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tvar err error\n\t\trules, err = s.getPolicyRules(ctx, policyIDs)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t}\n\t}()\n\n\tvar groupPeers []types.GroupPeer\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tvar err error\n\t\tgroupPeers, err = s.getGroupPeers(ctx, groupIDs)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t}\n\t}()\n\n\twg.Wait()\n\tclose(errChan)\n\tfor e := range errChan {\n\t\tif e != nil {\n\t\t\treturn nil, e\n\t\t}\n\t}\n\n\tpatsByUserID := make(map[string][]*types.PersonalAccessToken)\n\tfor i := range pats {\n\t\tpat := &pats[i]\n\t\tpatsByUserID[pat.UserID] = append(patsByUserID[pat.UserID], pat)\n\t\tpat.UserID = \"\"\n\t}\n\n\trulesByPolicyID := make(map[string][]*types.PolicyRule)\n\tfor _, rule := range rules {\n\t\trulesByPolicyID[rule.PolicyID] = append(rulesByPolicyID[rule.PolicyID], rule)\n\t}\n\n\tpeersByGroupID := make(map[string][]string)\n\tfor _, gp := range groupPeers {\n\t\tpeersByGroupID[gp.GroupID] = append(peersByGroupID[gp.GroupID], gp.PeerID)\n\t}\n\n\taccount.SetupKeys = make(map[string]*types.SetupKey, len(account.SetupKeysG))\n\tfor i := range account.SetupKeysG {\n\t\tkey := &account.SetupKeysG[i]\n\t\taccount.SetupKeys[key.Key] = key\n\t}\n\n\taccount.Peers = make(map[string]*nbpeer.Peer, len(account.PeersG))\n\tfor i := range account.PeersG {\n\t\tpeer := &account.PeersG[i]\n\t\taccount.Peers[peer.ID] = peer\n\t}\n\n\taccount.Users = make(map[string]*types.User, len(account.UsersG))\n\tfor i := range account.UsersG {\n\t\tuser := &account.UsersG[i]\n\t\tif err := user.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt user: %w\", err)\n\t\t}\n\t\tuser.PATs = make(map[string]*types.PersonalAccessToken)\n\t\tif userPats, ok := patsByUserID[user.Id]; ok {\n\t\t\tfor j := range userPats {\n\t\t\t\tpat := userPats[j]\n\t\t\t\tuser.PATs[pat.ID] = pat\n\t\t\t}\n\t\t}\n\t\taccount.Users[user.Id] = user\n\t}\n\n\tfor i := range account.Policies {\n\t\tpolicy := account.Policies[i]\n\t\tif policyRules, ok := rulesByPolicyID[policy.ID]; ok {\n\t\t\tpolicy.Rules = policyRules\n\t\t}\n\t}\n\n\taccount.Groups = make(map[string]*types.Group, len(account.GroupsG))\n\tfor i := range account.GroupsG {\n\t\tgroup := account.GroupsG[i]\n\t\tif peerIDs, ok := peersByGroupID[group.ID]; ok {\n\t\t\tgroup.Peers = peerIDs\n\t\t}\n\t\taccount.Groups[group.ID] = group\n\t}\n\n\taccount.Routes = make(map[route.ID]*route.Route, len(account.RoutesG))\n\tfor i := range account.RoutesG {\n\t\troute := &account.RoutesG[i]\n\t\taccount.Routes[route.ID] = route\n\t}\n\n\taccount.NameServerGroups = make(map[string]*nbdns.NameServerGroup, len(account.NameServerGroupsG))\n\tfor i := range account.NameServerGroupsG {\n\t\tnsg := &account.NameServerGroupsG[i]\n\t\tnsg.AccountID = \"\"\n\t\taccount.NameServerGroups[nsg.ID] = nsg\n\t}\n\n\taccount.SetupKeysG = nil\n\taccount.PeersG = nil\n\taccount.UsersG = nil\n\taccount.GroupsG = nil\n\taccount.RoutesG = nil\n\taccount.NameServerGroupsG = nil\n\n\treturn account, nil\n}\n\nfunc (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Account, error) {\n\tvar account types.Account\n\taccount.Network = &types.Network{}\n\tconst accountQuery = `\n\t\tSELECT\n\t\t\tid, created_by, created_at, domain, domain_category, is_domain_primary_account,\n\t\t\t-- Embedded Network\n\t\t\tnetwork_identifier, network_net, network_dns, network_serial,\n\t\t\t-- Embedded DNSSettings\n\t\t\tdns_settings_disabled_management_groups,\n\t\t\t-- Embedded Settings\n\t\t\tsettings_peer_login_expiration_enabled, settings_peer_login_expiration,\n\t\t\tsettings_peer_inactivity_expiration_enabled, settings_peer_inactivity_expiration,\n\t\t\tsettings_regular_users_view_blocked, settings_groups_propagation_enabled,\n\t\t\tsettings_jwt_groups_enabled, settings_jwt_groups_claim_name, settings_jwt_allow_groups,\n\t\t\tsettings_routing_peer_dns_resolution_enabled, settings_dns_domain, settings_network_range,\n\t\t\tsettings_lazy_connection_enabled,\n\t\t\t-- Embedded ExtraSettings\n\t\t\tsettings_extra_peer_approval_enabled, settings_extra_user_approval_required,\n\t\t\tsettings_extra_integrated_validator, settings_extra_integrated_validator_groups\n\t\tFROM accounts WHERE id = $1`\n\n\tvar (\n\t\tsPeerLoginExpirationEnabled sql.NullBool\n\t\tsPeerLoginExpiration sql.NullInt64\n\t\tsPeerInactivityExpirationEnabled sql.NullBool\n\t\tsPeerInactivityExpiration sql.NullInt64\n\t\tsRegularUsersViewBlocked sql.NullBool\n\t\tsGroupsPropagationEnabled sql.NullBool\n\t\tsJWTGroupsEnabled sql.NullBool\n\t\tsJWTGroupsClaimName sql.NullString\n\t\tsJWTAllowGroups sql.NullString\n\t\tsRoutingPeerDNSResolutionEnabled sql.NullBool\n\t\tsDNSDomain sql.NullString\n\t\tsNetworkRange sql.NullString\n\t\tsLazyConnectionEnabled sql.NullBool\n\t\tsExtraPeerApprovalEnabled sql.NullBool\n\t\tsExtraUserApprovalRequired sql.NullBool\n\t\tsExtraIntegratedValidator sql.NullString\n\t\tsExtraIntegratedValidatorGroups sql.NullString\n\t\tnetworkNet sql.NullString\n\t\tdnsSettingsDisabledGroups sql.NullString\n\t\tnetworkIdentifier sql.NullString\n\t\tnetworkDns sql.NullString\n\t\tnetworkSerial sql.NullInt64\n\t\tcreatedAt sql.NullTime\n\t)\n\terr := s.pool.QueryRow(ctx, accountQuery, accountID).Scan(\n\t\t&account.Id, &account.CreatedBy, &createdAt, &account.Domain, &account.DomainCategory, &account.IsDomainPrimaryAccount,\n\t\t&networkIdentifier, &networkNet, &networkDns, &networkSerial,\n\t\t&dnsSettingsDisabledGroups,\n\t\t&sPeerLoginExpirationEnabled, &sPeerLoginExpiration,\n\t\t&sPeerInactivityExpirationEnabled, &sPeerInactivityExpiration,\n\t\t&sRegularUsersViewBlocked, &sGroupsPropagationEnabled,\n\t\t&sJWTGroupsEnabled, &sJWTGroupsClaimName, &sJWTAllowGroups,\n\t\t&sRoutingPeerDNSResolutionEnabled, &sDNSDomain, &sNetworkRange,\n\t\t&sLazyConnectionEnabled,\n\t\t&sExtraPeerApprovalEnabled, &sExtraUserApprovalRequired,\n\t\t&sExtraIntegratedValidator, &sExtraIntegratedValidatorGroups,\n\t)\n\tif err != nil {\n\t\tif errors.Is(err, pgx.ErrNoRows) {\n\t\t\treturn nil, status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(err)\n\t}\n\n\taccount.Settings = &types.Settings{Extra: &types.ExtraSettings{}}\n\tif networkNet.Valid {\n\t\t_ = json.Unmarshal([]byte(networkNet.String), &account.Network.Net)\n\t}\n\tif createdAt.Valid {\n\t\taccount.CreatedAt = createdAt.Time\n\t}\n\tif dnsSettingsDisabledGroups.Valid {\n\t\t_ = json.Unmarshal([]byte(dnsSettingsDisabledGroups.String), &account.DNSSettings.DisabledManagementGroups)\n\t}\n\tif networkIdentifier.Valid {\n\t\taccount.Network.Identifier = networkIdentifier.String\n\t}\n\tif networkDns.Valid {\n\t\taccount.Network.Dns = networkDns.String\n\t}\n\tif networkSerial.Valid {\n\t\taccount.Network.Serial = uint64(networkSerial.Int64)\n\t}\n\tif sPeerLoginExpirationEnabled.Valid {\n\t\taccount.Settings.PeerLoginExpirationEnabled = sPeerLoginExpirationEnabled.Bool\n\t}\n\tif sPeerLoginExpiration.Valid {\n\t\taccount.Settings.PeerLoginExpiration = time.Duration(sPeerLoginExpiration.Int64)\n\t}\n\tif sPeerInactivityExpirationEnabled.Valid {\n\t\taccount.Settings.PeerInactivityExpirationEnabled = sPeerInactivityExpirationEnabled.Bool\n\t}\n\tif sPeerInactivityExpiration.Valid {\n\t\taccount.Settings.PeerInactivityExpiration = time.Duration(sPeerInactivityExpiration.Int64)\n\t}\n\tif sRegularUsersViewBlocked.Valid {\n\t\taccount.Settings.RegularUsersViewBlocked = sRegularUsersViewBlocked.Bool\n\t}\n\tif sGroupsPropagationEnabled.Valid {\n\t\taccount.Settings.GroupsPropagationEnabled = sGroupsPropagationEnabled.Bool\n\t}\n\tif sJWTGroupsEnabled.Valid {\n\t\taccount.Settings.JWTGroupsEnabled = sJWTGroupsEnabled.Bool\n\t}\n\tif sJWTGroupsClaimName.Valid {\n\t\taccount.Settings.JWTGroupsClaimName = sJWTGroupsClaimName.String\n\t}\n\tif sRoutingPeerDNSResolutionEnabled.Valid {\n\t\taccount.Settings.RoutingPeerDNSResolutionEnabled = sRoutingPeerDNSResolutionEnabled.Bool\n\t}\n\tif sDNSDomain.Valid {\n\t\taccount.Settings.DNSDomain = sDNSDomain.String\n\t}\n\tif sLazyConnectionEnabled.Valid {\n\t\taccount.Settings.LazyConnectionEnabled = sLazyConnectionEnabled.Bool\n\t}\n\tif sJWTAllowGroups.Valid {\n\t\t_ = json.Unmarshal([]byte(sJWTAllowGroups.String), &account.Settings.JWTAllowGroups)\n\t}\n\tif sNetworkRange.Valid {\n\t\t_ = json.Unmarshal([]byte(sNetworkRange.String), &account.Settings.NetworkRange)\n\t}\n\n\tif sExtraPeerApprovalEnabled.Valid {\n\t\taccount.Settings.Extra.PeerApprovalEnabled = sExtraPeerApprovalEnabled.Bool\n\t}\n\tif sExtraUserApprovalRequired.Valid {\n\t\taccount.Settings.Extra.UserApprovalRequired = sExtraUserApprovalRequired.Bool\n\t}\n\tif sExtraIntegratedValidator.Valid {\n\t\taccount.Settings.Extra.IntegratedValidator = sExtraIntegratedValidator.String\n\t}\n\tif sExtraIntegratedValidatorGroups.Valid {\n\t\t_ = json.Unmarshal([]byte(sExtraIntegratedValidatorGroups.String), &account.Settings.Extra.IntegratedValidatorGroups)\n\t}\n\taccount.InitOnce()\n\treturn &account, nil\n}\n\nfunc (s *SqlStore) getSetupKeys(ctx context.Context, accountID string) ([]types.SetupKey, error) {\n\tconst query = `SELECT id, account_id, key, key_secret, name, type, created_at, expires_at, updated_at, \n\trevoked, used_times, last_used, auto_groups, usage_limit, ephemeral, allow_extra_dns_labels FROM setup_keys WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tkeys, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (types.SetupKey, error) {\n\t\tvar sk types.SetupKey\n\t\tvar autoGroups []byte\n\t\tvar skCreatedAt, expiresAt, updatedAt, lastUsed sql.NullTime\n\t\tvar revoked, ephemeral, allowExtraDNSLabels sql.NullBool\n\t\tvar usedTimes, usageLimit sql.NullInt64\n\n\t\terr := row.Scan(&sk.Id, &sk.AccountID, &sk.Key, &sk.KeySecret, &sk.Name, &sk.Type, &skCreatedAt,\n\t\t\t&expiresAt, &updatedAt, &revoked, &usedTimes, &lastUsed, &autoGroups, &usageLimit, &ephemeral, &allowExtraDNSLabels)\n\n\t\tif err == nil {\n\t\t\tif expiresAt.Valid {\n\t\t\t\tsk.ExpiresAt = &expiresAt.Time\n\t\t\t}\n\t\t\tif skCreatedAt.Valid {\n\t\t\t\tsk.CreatedAt = skCreatedAt.Time\n\t\t\t}\n\t\t\tif updatedAt.Valid {\n\t\t\t\tsk.UpdatedAt = updatedAt.Time\n\t\t\t\tif sk.UpdatedAt.IsZero() {\n\t\t\t\t\tsk.UpdatedAt = sk.CreatedAt\n\t\t\t\t}\n\t\t\t}\n\t\t\tif lastUsed.Valid {\n\t\t\t\tsk.LastUsed = &lastUsed.Time\n\t\t\t}\n\t\t\tif revoked.Valid {\n\t\t\t\tsk.Revoked = revoked.Bool\n\t\t\t}\n\t\t\tif usedTimes.Valid {\n\t\t\t\tsk.UsedTimes = int(usedTimes.Int64)\n\t\t\t}\n\t\t\tif usageLimit.Valid {\n\t\t\t\tsk.UsageLimit = int(usageLimit.Int64)\n\t\t\t}\n\t\t\tif ephemeral.Valid {\n\t\t\t\tsk.Ephemeral = ephemeral.Bool\n\t\t\t}\n\t\t\tif allowExtraDNSLabels.Valid {\n\t\t\t\tsk.AllowExtraDNSLabels = allowExtraDNSLabels.Bool\n\t\t\t}\n\t\t\tif autoGroups != nil {\n\t\t\t\t_ = json.Unmarshal(autoGroups, &sk.AutoGroups)\n\t\t\t} else {\n\t\t\t\tsk.AutoGroups = []string{}\n\t\t\t}\n\t\t}\n\t\treturn sk, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn keys, nil\n}\n\nfunc (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Peer, error) {\n\tconst query = `SELECT id, account_id, key, ip, name, dns_label, user_id, ssh_key, ssh_enabled, login_expiration_enabled,\n\tinactivity_expiration_enabled, last_login, created_at, ephemeral, extra_dns_labels, allow_extra_dns_labels, meta_hostname, \n\tmeta_go_os, meta_kernel, meta_core, meta_platform, meta_os, meta_os_version, meta_wt_version, meta_ui_version, \n\tmeta_kernel_version, meta_network_addresses, meta_system_serial_number, meta_system_product_name, meta_system_manufacturer,\n\tmeta_environment, meta_flags, meta_files, peer_status_last_seen, peer_status_connected, peer_status_login_expired, \n\tpeer_status_requires_approval, location_connection_ip, location_country_code, location_city_name, \n\tlocation_geo_name_id, proxy_meta_embedded, proxy_meta_cluster FROM peers WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpeers, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (nbpeer.Peer, error) {\n\t\tvar p nbpeer.Peer\n\t\tp.Status = &nbpeer.PeerStatus{}\n\t\tvar (\n\t\t\tlastLogin, createdAt sql.NullTime\n\t\t\tsshEnabled, loginExpirationEnabled, inactivityExpirationEnabled, ephemeral, allowExtraDNSLabels sql.NullBool\n\t\t\tpeerStatusLastSeen sql.NullTime\n\t\t\tpeerStatusConnected, peerStatusLoginExpired, peerStatusRequiresApproval, proxyEmbedded sql.NullBool\n\t\t\tip, extraDNS, netAddr, env, flags, files, connIP []byte\n\t\t\tmetaHostname, metaGoOS, metaKernel, metaCore, metaPlatform sql.NullString\n\t\t\tmetaOS, metaOSVersion, metaWtVersion, metaUIVersion, metaKernelVersion sql.NullString\n\t\t\tmetaSystemSerialNumber, metaSystemProductName, metaSystemManufacturer sql.NullString\n\t\t\tlocationCountryCode, locationCityName, proxyCluster sql.NullString\n\t\t\tlocationGeoNameID sql.NullInt64\n\t\t)\n\n\t\terr := row.Scan(&p.ID, &p.AccountID, &p.Key, &ip, &p.Name, &p.DNSLabel, &p.UserID, &p.SSHKey, &sshEnabled,\n\t\t\t&loginExpirationEnabled, &inactivityExpirationEnabled, &lastLogin, &createdAt, &ephemeral, &extraDNS,\n\t\t\t&allowExtraDNSLabels, &metaHostname, &metaGoOS, &metaKernel, &metaCore, &metaPlatform,\n\t\t\t&metaOS, &metaOSVersion, &metaWtVersion, &metaUIVersion, &metaKernelVersion, &netAddr,\n\t\t\t&metaSystemSerialNumber, &metaSystemProductName, &metaSystemManufacturer, &env, &flags, &files,\n\t\t\t&peerStatusLastSeen, &peerStatusConnected, &peerStatusLoginExpired, &peerStatusRequiresApproval, &connIP,\n\t\t\t&locationCountryCode, &locationCityName, &locationGeoNameID, &proxyEmbedded, &proxyCluster)\n\n\t\tif err == nil {\n\t\t\tif lastLogin.Valid {\n\t\t\t\tp.LastLogin = &lastLogin.Time\n\t\t\t}\n\t\t\tif createdAt.Valid {\n\t\t\t\tp.CreatedAt = createdAt.Time\n\t\t\t}\n\t\t\tif sshEnabled.Valid {\n\t\t\t\tp.SSHEnabled = sshEnabled.Bool\n\t\t\t}\n\t\t\tif loginExpirationEnabled.Valid {\n\t\t\t\tp.LoginExpirationEnabled = loginExpirationEnabled.Bool\n\t\t\t}\n\t\t\tif inactivityExpirationEnabled.Valid {\n\t\t\t\tp.InactivityExpirationEnabled = inactivityExpirationEnabled.Bool\n\t\t\t}\n\t\t\tif ephemeral.Valid {\n\t\t\t\tp.Ephemeral = ephemeral.Bool\n\t\t\t}\n\t\t\tif allowExtraDNSLabels.Valid {\n\t\t\t\tp.AllowExtraDNSLabels = allowExtraDNSLabels.Bool\n\t\t\t}\n\t\t\tif peerStatusLastSeen.Valid {\n\t\t\t\tp.Status.LastSeen = peerStatusLastSeen.Time\n\t\t\t}\n\t\t\tif peerStatusConnected.Valid {\n\t\t\t\tp.Status.Connected = peerStatusConnected.Bool\n\t\t\t}\n\t\t\tif peerStatusLoginExpired.Valid {\n\t\t\t\tp.Status.LoginExpired = peerStatusLoginExpired.Bool\n\t\t\t}\n\t\t\tif peerStatusRequiresApproval.Valid {\n\t\t\t\tp.Status.RequiresApproval = peerStatusRequiresApproval.Bool\n\t\t\t}\n\t\t\tif metaHostname.Valid {\n\t\t\t\tp.Meta.Hostname = metaHostname.String\n\t\t\t}\n\t\t\tif metaGoOS.Valid {\n\t\t\t\tp.Meta.GoOS = metaGoOS.String\n\t\t\t}\n\t\t\tif metaKernel.Valid {\n\t\t\t\tp.Meta.Kernel = metaKernel.String\n\t\t\t}\n\t\t\tif metaCore.Valid {\n\t\t\t\tp.Meta.Core = metaCore.String\n\t\t\t}\n\t\t\tif metaPlatform.Valid {\n\t\t\t\tp.Meta.Platform = metaPlatform.String\n\t\t\t}\n\t\t\tif metaOS.Valid {\n\t\t\t\tp.Meta.OS = metaOS.String\n\t\t\t}\n\t\t\tif metaOSVersion.Valid {\n\t\t\t\tp.Meta.OSVersion = metaOSVersion.String\n\t\t\t}\n\t\t\tif metaWtVersion.Valid {\n\t\t\t\tp.Meta.WtVersion = metaWtVersion.String\n\t\t\t}\n\t\t\tif metaUIVersion.Valid {\n\t\t\t\tp.Meta.UIVersion = metaUIVersion.String\n\t\t\t}\n\t\t\tif metaKernelVersion.Valid {\n\t\t\t\tp.Meta.KernelVersion = metaKernelVersion.String\n\t\t\t}\n\t\t\tif metaSystemSerialNumber.Valid {\n\t\t\t\tp.Meta.SystemSerialNumber = metaSystemSerialNumber.String\n\t\t\t}\n\t\t\tif metaSystemProductName.Valid {\n\t\t\t\tp.Meta.SystemProductName = metaSystemProductName.String\n\t\t\t}\n\t\t\tif metaSystemManufacturer.Valid {\n\t\t\t\tp.Meta.SystemManufacturer = metaSystemManufacturer.String\n\t\t\t}\n\t\t\tif locationCountryCode.Valid {\n\t\t\t\tp.Location.CountryCode = locationCountryCode.String\n\t\t\t}\n\t\t\tif locationCityName.Valid {\n\t\t\t\tp.Location.CityName = locationCityName.String\n\t\t\t}\n\t\t\tif locationGeoNameID.Valid {\n\t\t\t\tp.Location.GeoNameID = uint(locationGeoNameID.Int64)\n\t\t\t}\n\t\t\tif proxyEmbedded.Valid {\n\t\t\t\tp.ProxyMeta.Embedded = proxyEmbedded.Bool\n\t\t\t}\n\t\t\tif proxyCluster.Valid {\n\t\t\t\tp.ProxyMeta.Cluster = proxyCluster.String\n\t\t\t}\n\t\t\tif ip != nil {\n\t\t\t\t_ = json.Unmarshal(ip, &p.IP)\n\t\t\t}\n\t\t\tif extraDNS != nil {\n\t\t\t\t_ = json.Unmarshal(extraDNS, &p.ExtraDNSLabels)\n\t\t\t}\n\t\t\tif netAddr != nil {\n\t\t\t\t_ = json.Unmarshal(netAddr, &p.Meta.NetworkAddresses)\n\t\t\t}\n\t\t\tif env != nil {\n\t\t\t\t_ = json.Unmarshal(env, &p.Meta.Environment)\n\t\t\t}\n\t\t\tif flags != nil {\n\t\t\t\t_ = json.Unmarshal(flags, &p.Meta.Flags)\n\t\t\t}\n\t\t\tif files != nil {\n\t\t\t\t_ = json.Unmarshal(files, &p.Meta.Files)\n\t\t\t}\n\t\t\tif connIP != nil {\n\t\t\t\t_ = json.Unmarshal(connIP, &p.Location.ConnectionIP)\n\t\t\t}\n\t\t}\n\t\treturn p, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn peers, nil\n}\n\nfunc (s *SqlStore) getUsers(ctx context.Context, accountID string) ([]types.User, error) {\n\tconst query = `SELECT id, account_id, role, is_service_user, non_deletable, service_user_name, auto_groups, blocked, pending_approval, last_login, created_at, issued, integration_ref_id, integration_ref_integration_type, email, name FROM users WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tusers, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (types.User, error) {\n\t\tvar u types.User\n\t\tvar autoGroups []byte\n\t\tvar lastLogin, createdAt sql.NullTime\n\t\tvar isServiceUser, nonDeletable, blocked, pendingApproval sql.NullBool\n\t\terr := row.Scan(&u.Id, &u.AccountID, &u.Role, &isServiceUser, &nonDeletable, &u.ServiceUserName, &autoGroups, &blocked, &pendingApproval, &lastLogin, &createdAt, &u.Issued, &u.IntegrationReference.ID, &u.IntegrationReference.IntegrationType, &u.Email, &u.Name)\n\t\tif err == nil {\n\t\t\tif lastLogin.Valid {\n\t\t\t\tu.LastLogin = &lastLogin.Time\n\t\t\t}\n\t\t\tif createdAt.Valid {\n\t\t\t\tu.CreatedAt = createdAt.Time\n\t\t\t}\n\t\t\tif isServiceUser.Valid {\n\t\t\t\tu.IsServiceUser = isServiceUser.Bool\n\t\t\t}\n\t\t\tif nonDeletable.Valid {\n\t\t\t\tu.NonDeletable = nonDeletable.Bool\n\t\t\t}\n\t\t\tif blocked.Valid {\n\t\t\t\tu.Blocked = blocked.Bool\n\t\t\t}\n\t\t\tif pendingApproval.Valid {\n\t\t\t\tu.PendingApproval = pendingApproval.Bool\n\t\t\t}\n\t\t\tif autoGroups != nil {\n\t\t\t\t_ = json.Unmarshal(autoGroups, &u.AutoGroups)\n\t\t\t} else {\n\t\t\t\tu.AutoGroups = []string{}\n\t\t\t}\n\t\t}\n\t\treturn u, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn users, nil\n}\n\nfunc (s *SqlStore) getGroups(ctx context.Context, accountID string) ([]*types.Group, error) {\n\tconst query = `SELECT id, account_id, name, issued, resources, integration_ref_id, integration_ref_integration_type FROM groups WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroups, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (*types.Group, error) {\n\t\tvar g types.Group\n\t\tvar resources []byte\n\t\tvar refID sql.NullInt64\n\t\tvar refType sql.NullString\n\t\terr := row.Scan(&g.ID, &g.AccountID, &g.Name, &g.Issued, &resources, &refID, &refType)\n\t\tif err == nil {\n\t\t\tif refID.Valid {\n\t\t\t\tg.IntegrationReference.ID = int(refID.Int64)\n\t\t\t}\n\t\t\tif refType.Valid {\n\t\t\t\tg.IntegrationReference.IntegrationType = refType.String\n\t\t\t}\n\t\t\tif resources != nil {\n\t\t\t\t_ = json.Unmarshal(resources, &g.Resources)\n\t\t\t} else {\n\t\t\t\tg.Resources = []types.Resource{}\n\t\t\t}\n\t\t\tg.GroupPeers = []types.GroupPeer{}\n\t\t\tg.Peers = []string{}\n\t\t}\n\t\treturn &g, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn groups, nil\n}\n\nfunc (s *SqlStore) getPolicies(ctx context.Context, accountID string) ([]*types.Policy, error) {\n\tconst query = `SELECT id, account_id, name, description, enabled, source_posture_checks FROM policies WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpolicies, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (*types.Policy, error) {\n\t\tvar p types.Policy\n\t\tvar checks []byte\n\t\tvar enabled sql.NullBool\n\t\terr := row.Scan(&p.ID, &p.AccountID, &p.Name, &p.Description, &enabled, &checks)\n\t\tif err == nil {\n\t\t\tif enabled.Valid {\n\t\t\t\tp.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif checks != nil {\n\t\t\t\t_ = json.Unmarshal(checks, &p.SourcePostureChecks)\n\t\t\t}\n\t\t}\n\t\treturn &p, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn policies, nil\n}\n\nfunc (s *SqlStore) getRoutes(ctx context.Context, accountID string) ([]route.Route, error) {\n\tconst query = `SELECT id, account_id, network, domains, keep_route, net_id, description, peer, peer_groups, network_type, masquerade, metric, enabled, groups, access_control_groups, skip_auto_apply FROM routes WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\troutes, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (route.Route, error) {\n\t\tvar r route.Route\n\t\tvar network, domains, peerGroups, groups, accessGroups []byte\n\t\tvar keepRoute, masquerade, enabled, skipAutoApply sql.NullBool\n\t\tvar metric sql.NullInt64\n\t\terr := row.Scan(&r.ID, &r.AccountID, &network, &domains, &keepRoute, &r.NetID, &r.Description, &r.Peer, &peerGroups, &r.NetworkType, &masquerade, &metric, &enabled, &groups, &accessGroups, &skipAutoApply)\n\t\tif err == nil {\n\t\t\tif keepRoute.Valid {\n\t\t\t\tr.KeepRoute = keepRoute.Bool\n\t\t\t}\n\t\t\tif masquerade.Valid {\n\t\t\t\tr.Masquerade = masquerade.Bool\n\t\t\t}\n\t\t\tif enabled.Valid {\n\t\t\t\tr.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif skipAutoApply.Valid {\n\t\t\t\tr.SkipAutoApply = skipAutoApply.Bool\n\t\t\t}\n\t\t\tif metric.Valid {\n\t\t\t\tr.Metric = int(metric.Int64)\n\t\t\t}\n\t\t\tif network != nil {\n\t\t\t\t_ = json.Unmarshal(network, &r.Network)\n\t\t\t}\n\t\t\tif domains != nil {\n\t\t\t\t_ = json.Unmarshal(domains, &r.Domains)\n\t\t\t}\n\t\t\tif peerGroups != nil {\n\t\t\t\t_ = json.Unmarshal(peerGroups, &r.PeerGroups)\n\t\t\t}\n\t\t\tif groups != nil {\n\t\t\t\t_ = json.Unmarshal(groups, &r.Groups)\n\t\t\t}\n\t\t\tif accessGroups != nil {\n\t\t\t\t_ = json.Unmarshal(accessGroups, &r.AccessControlGroups)\n\t\t\t}\n\t\t}\n\t\treturn r, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn routes, nil\n}\n\nfunc (s *SqlStore) getNameServerGroups(ctx context.Context, accountID string) ([]nbdns.NameServerGroup, error) {\n\tconst query = `SELECT id, account_id, name, description, name_servers, groups, \"primary\", domains, enabled, search_domains_enabled FROM name_server_groups WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tnsgs, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (nbdns.NameServerGroup, error) {\n\t\tvar n nbdns.NameServerGroup\n\t\tvar ns, groups, domains []byte\n\t\tvar primary, enabled, searchDomainsEnabled sql.NullBool\n\t\terr := row.Scan(&n.ID, &n.AccountID, &n.Name, &n.Description, &ns, &groups, &primary, &domains, &enabled, &searchDomainsEnabled)\n\t\tif err == nil {\n\t\t\tif primary.Valid {\n\t\t\t\tn.Primary = primary.Bool\n\t\t\t}\n\t\t\tif enabled.Valid {\n\t\t\t\tn.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif searchDomainsEnabled.Valid {\n\t\t\t\tn.SearchDomainsEnabled = searchDomainsEnabled.Bool\n\t\t\t}\n\t\t\tif ns != nil {\n\t\t\t\t_ = json.Unmarshal(ns, &n.NameServers)\n\t\t\t} else {\n\t\t\t\tn.NameServers = []nbdns.NameServer{}\n\t\t\t}\n\t\t\tif groups != nil {\n\t\t\t\t_ = json.Unmarshal(groups, &n.Groups)\n\t\t\t} else {\n\t\t\t\tn.Groups = []string{}\n\t\t\t}\n\t\t\tif domains != nil {\n\t\t\t\t_ = json.Unmarshal(domains, &n.Domains)\n\t\t\t} else {\n\t\t\t\tn.Domains = []string{}\n\t\t\t}\n\t\t}\n\t\treturn n, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn nsgs, nil\n}\n\nfunc (s *SqlStore) getPostureChecks(ctx context.Context, accountID string) ([]*posture.Checks, error) {\n\tconst query = `SELECT id, account_id, name, description, checks FROM posture_checks WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tchecks, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (*posture.Checks, error) {\n\t\tvar c posture.Checks\n\t\tvar checksDef []byte\n\t\terr := row.Scan(&c.ID, &c.AccountID, &c.Name, &c.Description, &checksDef)\n\t\tif err == nil && checksDef != nil {\n\t\t\t_ = json.Unmarshal(checksDef, &c.Checks)\n\t\t}\n\t\treturn &c, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn checks, nil\n}\n\nfunc (s *SqlStore) getServices(ctx context.Context, accountID string) ([]*reverseproxy.Service, error) {\n\tconst serviceQuery = `SELECT id, account_id, name, domain, enabled, auth,\n\t\tmeta_created_at, meta_certificate_issued_at, meta_status, proxy_cluster,\n\t\tpass_host_header, rewrite_redirects, session_private_key, session_public_key\n\t\tFROM services WHERE account_id = $1`\n\n\tconst targetsQuery = `SELECT id, account_id, service_id, path, host, port, protocol,\n\t\ttarget_id, target_type, enabled\n\t\tFROM targets WHERE service_id = ANY($1)`\n\n\tserviceRows, err := s.pool.Query(ctx, serviceQuery, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tservices, err := pgx.CollectRows(serviceRows, func(row pgx.CollectableRow) (*reverseproxy.Service, error) {\n\t\tvar s reverseproxy.Service\n\t\tvar auth []byte\n\t\tvar createdAt, certIssuedAt sql.NullTime\n\t\tvar status, proxyCluster, sessionPrivateKey, sessionPublicKey sql.NullString\n\t\terr := row.Scan(\n\t\t\t&s.ID,\n\t\t\t&s.AccountID,\n\t\t\t&s.Name,\n\t\t\t&s.Domain,\n\t\t\t&s.Enabled,\n\t\t\t&auth,\n\t\t\t&createdAt,\n\t\t\t&certIssuedAt,\n\t\t\t&status,\n\t\t\t&proxyCluster,\n\t\t\t&s.PassHostHeader,\n\t\t\t&s.RewriteRedirects,\n\t\t\t&sessionPrivateKey,\n\t\t\t&sessionPublicKey,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif auth != nil {\n\t\t\tif err := json.Unmarshal(auth, &s.Auth); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\ts.Meta = reverseproxy.ServiceMeta{}\n\t\tif createdAt.Valid {\n\t\t\ts.Meta.CreatedAt = createdAt.Time\n\t\t}\n\t\tif certIssuedAt.Valid {\n\t\t\tt := certIssuedAt.Time\n\t\t\ts.Meta.CertificateIssuedAt = &t\n\t\t}\n\t\tif status.Valid {\n\t\t\ts.Meta.Status = status.String\n\t\t}\n\t\tif proxyCluster.Valid {\n\t\t\ts.ProxyCluster = proxyCluster.String\n\t\t}\n\t\tif sessionPrivateKey.Valid {\n\t\t\ts.SessionPrivateKey = sessionPrivateKey.String\n\t\t}\n\t\tif sessionPublicKey.Valid {\n\t\t\ts.SessionPublicKey = sessionPublicKey.String\n\t\t}\n\n\t\ts.Targets = []*reverseproxy.Target{}\n\t\treturn &s, nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(services) == 0 {\n\t\treturn services, nil\n\t}\n\n\tserviceIDs := make([]string, len(services))\n\tserviceMap := make(map[string]*reverseproxy.Service)\n\tfor i, s := range services {\n\t\tserviceIDs[i] = s.ID\n\t\tserviceMap[s.ID] = s\n\t}\n\n\ttargetRows, err := s.pool.Query(ctx, targetsQuery, serviceIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttargets, err := pgx.CollectRows(targetRows, func(row pgx.CollectableRow) (*reverseproxy.Target, error) {\n\t\tvar t reverseproxy.Target\n\t\tvar path sql.NullString\n\t\terr := row.Scan(\n\t\t\t&t.ID,\n\t\t\t&t.AccountID,\n\t\t\t&t.ServiceID,\n\t\t\t&path,\n\t\t\t&t.Host,\n\t\t\t&t.Port,\n\t\t\t&t.Protocol,\n\t\t\t&t.TargetId,\n\t\t\t&t.TargetType,\n\t\t\t&t.Enabled,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif path.Valid {\n\t\t\tt.Path = &path.String\n\t\t}\n\t\treturn &t, nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, target := range targets {\n\t\tif service, ok := serviceMap[target.ServiceID]; ok {\n\t\t\tservice.Targets = append(service.Targets, target)\n\t\t}\n\t}\n\n\treturn services, nil\n}\n\nfunc (s *SqlStore) getNetworks(ctx context.Context, accountID string) ([]*networkTypes.Network, error) {\n\tconst query = `SELECT id, account_id, name, description FROM networks WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tnetworks, err := pgx.CollectRows(rows, pgx.RowToStructByName[networkTypes.Network])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult := make([]*networkTypes.Network, len(networks))\n\tfor i := range networks {\n\t\tresult[i] = &networks[i]\n\t}\n\treturn result, nil\n}\n\nfunc (s *SqlStore) getNetworkRouters(ctx context.Context, accountID string) ([]*routerTypes.NetworkRouter, error) {\n\tconst query = `SELECT id, network_id, account_id, peer, peer_groups, masquerade, metric, enabled FROM network_routers WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trouters, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (routerTypes.NetworkRouter, error) {\n\t\tvar r routerTypes.NetworkRouter\n\t\tvar peerGroups []byte\n\t\tvar masquerade, enabled sql.NullBool\n\t\tvar metric sql.NullInt64\n\t\terr := row.Scan(&r.ID, &r.NetworkID, &r.AccountID, &r.Peer, &peerGroups, &masquerade, &metric, &enabled)\n\t\tif err == nil {\n\t\t\tif masquerade.Valid {\n\t\t\t\tr.Masquerade = masquerade.Bool\n\t\t\t}\n\t\t\tif enabled.Valid {\n\t\t\t\tr.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif metric.Valid {\n\t\t\t\tr.Metric = int(metric.Int64)\n\t\t\t}\n\t\t\tif peerGroups != nil {\n\t\t\t\t_ = json.Unmarshal(peerGroups, &r.PeerGroups)\n\t\t\t}\n\t\t}\n\t\treturn r, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult := make([]*routerTypes.NetworkRouter, len(routers))\n\tfor i := range routers {\n\t\tresult[i] = &routers[i]\n\t}\n\treturn result, nil\n}\n\nfunc (s *SqlStore) getNetworkResources(ctx context.Context, accountID string) ([]*resourceTypes.NetworkResource, error) {\n\tconst query = `SELECT id, network_id, account_id, name, description, type, domain, prefix, enabled FROM network_resources WHERE account_id = $1`\n\trows, err := s.pool.Query(ctx, query, accountID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresources, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (resourceTypes.NetworkResource, error) {\n\t\tvar r resourceTypes.NetworkResource\n\t\tvar prefix []byte\n\t\tvar enabled sql.NullBool\n\t\terr := row.Scan(&r.ID, &r.NetworkID, &r.AccountID, &r.Name, &r.Description, &r.Type, &r.Domain, &prefix, &enabled)\n\t\tif err == nil {\n\t\t\tif enabled.Valid {\n\t\t\t\tr.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif prefix != nil {\n\t\t\t\t_ = json.Unmarshal(prefix, &r.Prefix)\n\t\t\t}\n\t\t}\n\t\treturn r, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult := make([]*resourceTypes.NetworkResource, len(resources))\n\tfor i := range resources {\n\t\tresult[i] = &resources[i]\n\t}\n\treturn result, nil\n}\n\nfunc (s *SqlStore) getAccountOnboarding(ctx context.Context, accountID string, account *types.Account) error {\n\tconst query = `SELECT account_id, onboarding_flow_pending, signup_form_pending, created_at, updated_at FROM account_onboardings WHERE account_id = $1`\n\tvar onboardingFlowPending, signupFormPending sql.NullBool\n\tvar createdAt, updatedAt sql.NullTime\n\terr := s.pool.QueryRow(ctx, query, accountID).Scan(\n\t\t&account.Onboarding.AccountID,\n\t\t&onboardingFlowPending,\n\t\t&signupFormPending,\n\t\t&createdAt,\n\t\t&updatedAt,\n\t)\n\tif err != nil && !errors.Is(err, pgx.ErrNoRows) {\n\t\treturn err\n\t}\n\tif createdAt.Valid {\n\t\taccount.Onboarding.CreatedAt = createdAt.Time\n\t}\n\tif updatedAt.Valid {\n\t\taccount.Onboarding.UpdatedAt = updatedAt.Time\n\t}\n\tif onboardingFlowPending.Valid {\n\t\taccount.Onboarding.OnboardingFlowPending = onboardingFlowPending.Bool\n\t}\n\tif signupFormPending.Valid {\n\t\taccount.Onboarding.SignupFormPending = signupFormPending.Bool\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) getPersonalAccessTokens(ctx context.Context, userIDs []string) ([]types.PersonalAccessToken, error) {\n\tif len(userIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\tconst query = `SELECT id, user_id, name, hashed_token, expiration_date, created_by, created_at, last_used FROM personal_access_tokens WHERE user_id = ANY($1)`\n\trows, err := s.pool.Query(ctx, query, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpats, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (types.PersonalAccessToken, error) {\n\t\tvar pat types.PersonalAccessToken\n\t\tvar expirationDate, lastUsed, createdAt sql.NullTime\n\t\terr := row.Scan(&pat.ID, &pat.UserID, &pat.Name, &pat.HashedToken, &expirationDate, &pat.CreatedBy, &createdAt, &lastUsed)\n\t\tif err == nil {\n\t\t\tif expirationDate.Valid {\n\t\t\t\tpat.ExpirationDate = &expirationDate.Time\n\t\t\t}\n\t\t\tif createdAt.Valid {\n\t\t\t\tpat.CreatedAt = createdAt.Time\n\t\t\t}\n\t\t\tif lastUsed.Valid {\n\t\t\t\tpat.LastUsed = &lastUsed.Time\n\t\t\t}\n\t\t}\n\t\treturn pat, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn pats, nil\n}\n\nfunc (s *SqlStore) getPolicyRules(ctx context.Context, policyIDs []string) ([]*types.PolicyRule, error) {\n\tif len(policyIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\tconst query = `SELECT id, policy_id, name, description, enabled, action, destinations, destination_resource, sources, source_resource, bidirectional, protocol, ports, port_ranges, authorized_groups, authorized_user FROM policy_rules WHERE policy_id = ANY($1)`\n\trows, err := s.pool.Query(ctx, query, policyIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trules, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (*types.PolicyRule, error) {\n\t\tvar r types.PolicyRule\n\t\tvar dest, destRes, sources, sourceRes, ports, portRanges, authorizedGroups []byte\n\t\tvar enabled, bidirectional sql.NullBool\n\t\tvar authorizedUser sql.NullString\n\t\terr := row.Scan(&r.ID, &r.PolicyID, &r.Name, &r.Description, &enabled, &r.Action, &dest, &destRes, &sources, &sourceRes, &bidirectional, &r.Protocol, &ports, &portRanges, &authorizedGroups, &authorizedUser)\n\t\tif err == nil {\n\t\t\tif enabled.Valid {\n\t\t\t\tr.Enabled = enabled.Bool\n\t\t\t}\n\t\t\tif bidirectional.Valid {\n\t\t\t\tr.Bidirectional = bidirectional.Bool\n\t\t\t}\n\t\t\tif dest != nil {\n\t\t\t\t_ = json.Unmarshal(dest, &r.Destinations)\n\t\t\t}\n\t\t\tif destRes != nil {\n\t\t\t\t_ = json.Unmarshal(destRes, &r.DestinationResource)\n\t\t\t}\n\t\t\tif sources != nil {\n\t\t\t\t_ = json.Unmarshal(sources, &r.Sources)\n\t\t\t}\n\t\t\tif sourceRes != nil {\n\t\t\t\t_ = json.Unmarshal(sourceRes, &r.SourceResource)\n\t\t\t}\n\t\t\tif ports != nil {\n\t\t\t\t_ = json.Unmarshal(ports, &r.Ports)\n\t\t\t}\n\t\t\tif portRanges != nil {\n\t\t\t\t_ = json.Unmarshal(portRanges, &r.PortRanges)\n\t\t\t}\n\t\t\tif authorizedGroups != nil {\n\t\t\t\t_ = json.Unmarshal(authorizedGroups, &r.AuthorizedGroups)\n\t\t\t}\n\t\t\tif authorizedUser.Valid {\n\t\t\t\tr.AuthorizedUser = authorizedUser.String\n\t\t\t}\n\t\t}\n\t\treturn &r, err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn rules, nil\n}\n\nfunc (s *SqlStore) getGroupPeers(ctx context.Context, groupIDs []string) ([]types.GroupPeer, error) {\n\tif len(groupIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\tconst query = `SELECT account_id, group_id, peer_id FROM group_peers WHERE group_id = ANY($1)`\n\trows, err := s.pool.Query(ctx, query, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroupPeers, err := pgx.CollectRows(rows, pgx.RowToStructByName[types.GroupPeer])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn groupPeers, nil\n}\n\nfunc (s *SqlStore) GetAccountByUser(ctx context.Context, userID string) (*types.Account, error) {\n\tvar user types.User\n\tresult := s.db.Select(\"account_id\").Take(&user, idQueryCondition, userID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\tif user.AccountID == \"\" {\n\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn s.GetAccount(ctx, user.AccountID)\n}\n\nfunc (s *SqlStore) GetAccountByPeerID(ctx context.Context, peerID string) (*types.Account, error) {\n\tvar peer nbpeer.Peer\n\tresult := s.db.Select(\"account_id\").Take(&peer, idQueryCondition, peerID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\tif peer.AccountID == \"\" {\n\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn s.GetAccount(ctx, peer.AccountID)\n}\n\nfunc (s *SqlStore) GetAccountByPeerPubKey(ctx context.Context, peerKey string) (*types.Account, error) {\n\tvar peer nbpeer.Peer\n\tresult := s.db.Select(\"account_id\").Take(&peer, GetKeyQueryCondition(s), peerKey)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\treturn nil, status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\tif peer.AccountID == \"\" {\n\t\treturn nil, status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn s.GetAccount(ctx, peer.AccountID)\n}\n\nfunc (s *SqlStore) GetAnyAccountID(ctx context.Context) (string, error) {\n\tvar account types.Account\n\tresult := s.db.Select(\"id\").Order(\"created_at desc\").Limit(1).Find(&account)\n\tif result.Error != nil {\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\tif result.RowsAffected == 0 {\n\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn account.Id, nil\n}\n\nfunc (s *SqlStore) GetAccountIDByPeerPubKey(ctx context.Context, peerKey string) (string, error) {\n\tvar peer nbpeer.Peer\n\tvar accountID string\n\tresult := s.db.Model(&peer).Select(\"account_id\").Where(GetKeyQueryCondition(s), peerKey).Take(&accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn accountID, nil\n}\n\nfunc (s *SqlStore) GetAccountIDByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountID string\n\tresult := tx.Model(&types.User{}).\n\t\tSelect(\"account_id\").Where(idQueryCondition, userID).Take(&accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t\t}\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn accountID, nil\n}\n\nfunc (s *SqlStore) GetAccountIDByPeerID(ctx context.Context, lockStrength LockingStrength, peerID string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountID string\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tSelect(\"account_id\").Where(idQueryCondition, peerID).Take(&accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"peer %s account not found\", peerID)\n\t\t}\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn accountID, nil\n}\n\nfunc (s *SqlStore) GetAccountIDBySetupKey(ctx context.Context, setupKey string) (string, error) {\n\tvar accountID string\n\tresult := s.db.Model(&types.SetupKey{}).Select(\"account_id\").Where(GetKeyQueryCondition(s), setupKey).Take(&accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.NewSetupKeyNotFoundError(setupKey)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get account ID by setup key from store: %v\", result.Error)\n\t\treturn \"\", status.Errorf(status.Internal, \"failed to get account ID by setup key from store\")\n\t}\n\n\tif accountID == \"\" {\n\t\treturn \"\", status.Errorf(status.NotFound, \"account not found: index lookup failed\")\n\t}\n\n\treturn accountID, nil\n}\n\nfunc (s *SqlStore) GetTakenIPs(ctx context.Context, lockStrength LockingStrength, accountID string) ([]net.IP, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar ipJSONStrings []string\n\n\t// Fetch the IP addresses as JSON strings\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tWhere(\"account_id = ?\", accountID).\n\t\tPluck(\"ip\", &ipJSONStrings)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"no peers found for the account\")\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting IPs from store: %s\", result.Error)\n\t}\n\n\t// Convert the JSON strings to net.IP objects\n\tips := make([]net.IP, len(ipJSONStrings))\n\tfor i, ipJSON := range ipJSONStrings {\n\t\tvar ip net.IP\n\t\tif err := json.Unmarshal([]byte(ipJSON), &ip); err != nil {\n\t\t\treturn nil, status.Errorf(status.Internal, \"issue parsing IP JSON from store\")\n\t\t}\n\t\tips[i] = ip\n\t}\n\n\treturn ips, nil\n}\n\nfunc (s *SqlStore) GetPeerLabelsInAccount(ctx context.Context, lockStrength LockingStrength, accountID string, dnsLabel string) ([]string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar labels []string\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tWhere(\"account_id = ? AND dns_label LIKE ?\", accountID, dnsLabel+\"%\").\n\t\tPluck(\"dns_label\", &labels)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"no peers found for the account\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"error when getting dns labels from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting dns labels from store: %s\", result.Error)\n\t}\n\n\treturn labels, nil\n}\n\nfunc (s *SqlStore) GetAccountNetwork(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.Network, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountNetwork types.AccountNetwork\n\tif err := tx.Model(&types.Account{}).Where(idQueryCondition, accountID).Take(&accountNetwork).Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting network from store: %s\", err)\n\t}\n\treturn accountNetwork.Network, nil\n}\n\nfunc (s *SqlStore) GetPeerByPeerPubKey(ctx context.Context, lockStrength LockingStrength, peerKey string) (*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peer nbpeer.Peer\n\tresult := tx.Take(&peer, GetKeyQueryCondition(s), peerKey)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPeerNotFoundError(peerKey)\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting peer from store: %s\", result.Error)\n\t}\n\n\treturn &peer, nil\n}\n\nfunc (s *SqlStore) GetAccountSettings(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.Settings, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountSettings types.AccountSettings\n\tif err := tx.Model(&types.Account{}).Where(idQueryCondition, accountID).Take(&accountSettings).Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"settings not found\")\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"issue getting settings from store: %s\", err)\n\t}\n\treturn accountSettings.Settings, nil\n}\n\nfunc (s *SqlStore) GetAccountCreatedBy(ctx context.Context, lockStrength LockingStrength, accountID string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar createdBy string\n\tresult := tx.Model(&types.Account{}).\n\t\tSelect(\"created_by\").Take(&createdBy, idQueryCondition, accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\treturn \"\", status.NewGetAccountFromStoreError(result.Error)\n\t}\n\n\treturn createdBy, nil\n}\n\n// SaveUserLastLogin stores the last login time for a user in DB.\nfunc (s *SqlStore) SaveUserLastLogin(ctx context.Context, accountID, userID string, lastLogin time.Time) error {\n\tvar user types.User\n\tresult := s.db.Take(&user, accountAndIDQueryCondition, accountID, userID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn status.NewUserNotFoundError(userID)\n\t\t}\n\t\treturn status.NewGetUserFromStoreError()\n\t}\n\n\tif !lastLogin.IsZero() {\n\t\tuser.LastLogin = &lastLogin\n\t\treturn s.db.Save(&user).Error\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetPostureCheckByChecksDefinition(accountID string, checks *posture.ChecksDefinition) (*posture.Checks, error) {\n\tdefinitionJSON, err := json.Marshal(checks)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar postureCheck posture.Checks\n\terr = s.db.Where(\"account_id = ? AND checks = ?\", accountID, string(definitionJSON)).Take(&postureCheck).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &postureCheck, nil\n}\n\n// Close closes the underlying DB connection\nfunc (s *SqlStore) Close(_ context.Context) error {\n\tsql, err := s.db.DB()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get db: %w\", err)\n\t}\n\treturn sql.Close()\n}\n\n// GetStoreEngine returns underlying store engine\nfunc (s *SqlStore) GetStoreEngine() types.Engine {\n\treturn s.storeEngine\n}\n\n// NewSqliteStore creates a new SQLite store.\nfunc NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tstoreStr := fmt.Sprintf(\"%s?cache=shared\", storeSqliteFileName)\n\tif runtime.GOOS == \"windows\" {\n\t\t// Vo avoid `The process cannot access the file because it is being used by another process` on Windows\n\t\tstoreStr = storeSqliteFileName\n\t}\n\n\tfile := filepath.Join(dataDir, storeStr)\n\tdb, err := gorm.Open(sqlite.Open(file), getGormConfig())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn NewSqlStore(ctx, db, types.SqliteStoreEngine, metrics, skipMigration)\n}\n\n// NewPostgresqlStore creates a new Postgres store.\nfunc NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tdb, err := gorm.Open(postgres.Open(dsn), getGormConfig())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpool, err := connectToPgDb(context.Background(), dsn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tstore, err := NewSqlStore(ctx, db, types.PostgresStoreEngine, metrics, skipMigration)\n\tif err != nil {\n\t\tpool.Close()\n\t\treturn nil, err\n\t}\n\tstore.pool = pool\n\treturn store, nil\n}\n\nfunc connectToPgDb(ctx context.Context, dsn string) (*pgxpool.Pool, error) {\n\tconfig, err := pgxpool.ParseConfig(dsn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse database config: %w\", err)\n\t}\n\n\tconfig.MaxConns = pgMaxConnections\n\tconfig.MinConns = pgMinConnections\n\tconfig.MaxConnLifetime = pgMaxConnLifetime\n\tconfig.HealthCheckPeriod = pgHealthCheckPeriod\n\n\tpool, err := pgxpool.NewWithConfig(ctx, config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create connection pool: %w\", err)\n\t}\n\n\tif err := pool.Ping(ctx); err != nil {\n\t\tpool.Close()\n\t\treturn nil, fmt.Errorf(\"unable to ping database: %w\", err)\n\t}\n\n\treturn pool, nil\n}\n\n// NewMysqlStore creates a new MySQL store.\nfunc NewMysqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tdb, err := gorm.Open(mysql.Open(dsn+\"?charset=utf8&parseTime=True&loc=Local\"), getGormConfig())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn NewSqlStore(ctx, db, types.MysqlStoreEngine, metrics, skipMigration)\n}\n\nfunc getGormConfig() *gorm.Config {\n\treturn &gorm.Config{\n\t\tLogger: logger.Default.LogMode(logger.Silent),\n\t\tCreateBatchSize: 400,\n\t}\n}\n\n// newPostgresStore initializes a new Postgres store.\nfunc newPostgresStore(ctx context.Context, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {\n\tdsn, ok := lookupDSNEnv(postgresDsnEnv, postgresDsnEnvLegacy)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"%s is not set\", postgresDsnEnv)\n\t}\n\treturn NewPostgresqlStore(ctx, dsn, metrics, skipMigration)\n}\n\n// newMysqlStore initializes a new MySQL store.\nfunc newMysqlStore(ctx context.Context, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {\n\tdsn, ok := lookupDSNEnv(mysqlDsnEnv, mysqlDsnEnvLegacy)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"%s is not set\", mysqlDsnEnv)\n\t}\n\treturn NewMysqlStore(ctx, dsn, metrics, skipMigration)\n}\n\n// NewSqliteStoreFromFileStore restores a store from FileStore and stores SQLite DB in the file located in datadir.\nfunc NewSqliteStoreFromFileStore(ctx context.Context, fileStore *FileStore, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tstore, err := NewSqliteStore(ctx, dataDir, metrics, skipMigration)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = store.SaveInstallationID(ctx, fileStore.InstallationID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, account := range fileStore.GetAllAccounts(ctx) {\n\t\t_, err = account.GetGroupAll()\n\t\tif err != nil {\n\t\t\tif err := account.AddAllGroup(false); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\terr := store.SaveAccount(ctx, account)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn store, nil\n}\n\n// NewPostgresqlStoreFromSqlStore restores a store from SqlStore and stores Postgres DB.\nfunc NewPostgresqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) {\n\tstore, err := NewPostgresqlStoreForTests(ctx, dsn, metrics, false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = store.SaveInstallationID(ctx, sqliteStore.GetInstallationID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, account := range sqliteStore.GetAllAccounts(ctx) {\n\t\terr := store.SaveAccount(ctx, account)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn store, nil\n}\n\n// used for tests only\nfunc NewPostgresqlStoreForTests(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {\n\tdb, err := gorm.Open(postgres.Open(dsn), getGormConfig())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpool, err := connectToPgDbForTests(context.Background(), dsn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tstore, err := NewSqlStore(ctx, db, types.PostgresStoreEngine, metrics, skipMigration)\n\tif err != nil {\n\t\tpool.Close()\n\t\treturn nil, err\n\t}\n\tstore.pool = pool\n\treturn store, nil\n}\n\n// used for tests only\nfunc connectToPgDbForTests(ctx context.Context, dsn string) (*pgxpool.Pool, error) {\n\tconfig, err := pgxpool.ParseConfig(dsn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse database config: %w\", err)\n\t}\n\n\tconfig.MaxConns = 5\n\tconfig.MinConns = 1\n\tconfig.MaxConnLifetime = 30 * time.Second\n\tconfig.HealthCheckPeriod = 10 * time.Second\n\n\tpool, err := pgxpool.NewWithConfig(ctx, config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create connection pool: %w\", err)\n\t}\n\n\tif err := pool.Ping(ctx); err != nil {\n\t\tpool.Close()\n\t\treturn nil, fmt.Errorf(\"unable to ping database: %w\", err)\n\t}\n\n\treturn pool, nil\n}\n\n// NewMysqlStoreFromSqlStore restores a store from SqlStore and stores MySQL DB.\nfunc NewMysqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) {\n\tstore, err := NewMysqlStore(ctx, dsn, metrics, false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = store.SaveInstallationID(ctx, sqliteStore.GetInstallationID())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, account := range sqliteStore.GetAllAccounts(ctx) {\n\t\terr := store.SaveAccount(ctx, account)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn store, nil\n}\n\nfunc (s *SqlStore) GetSetupKeyBySecret(ctx context.Context, lockStrength LockingStrength, key string) (*types.SetupKey, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar setupKey types.SetupKey\n\tresult := tx.\n\t\tTake(&setupKey, GetKeyQueryCondition(s), key)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.PreconditionFailed, \"setup key not found\")\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get setup key by secret from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get setup key by secret from store\")\n\t}\n\treturn &setupKey, nil\n}\n\nfunc (s *SqlStore) IncrementSetupKeyUsage(ctx context.Context, setupKeyID string) error {\n\tresult := s.db.Model(&types.SetupKey{}).\n\t\tWhere(idQueryCondition, setupKeyID).\n\t\tUpdates(map[string]interface{}{\n\t\t\t\"used_times\": gorm.Expr(\"used_times + 1\"),\n\t\t\t\"last_used\": time.Now(),\n\t\t})\n\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"issue incrementing setup key usage count: %s\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewSetupKeyNotFoundError(setupKeyID)\n\t}\n\n\treturn nil\n}\n\n// AddPeerToAllGroup adds a peer to the 'All' group. Method always needs to run in a transaction\nfunc (s *SqlStore) AddPeerToAllGroup(ctx context.Context, accountID string, peerID string) error {\n\tvar groupID string\n\t_ = s.db.Model(types.Group{}).\n\t\tSelect(\"id\").\n\t\tWhere(\"account_id = ? AND name = ?\", accountID, \"All\").\n\t\tLimit(1).\n\t\tScan(&groupID)\n\n\tif groupID == \"\" {\n\t\treturn status.Errorf(status.NotFound, \"group 'All' not found for account %s\", accountID)\n\t}\n\n\terr := s.db.Clauses(clause.OnConflict{\n\t\tColumns: []clause.Column{{Name: \"group_id\"}, {Name: \"peer_id\"}},\n\t\tDoNothing: true,\n\t}).Create(&types.GroupPeer{\n\t\tAccountID: accountID,\n\t\tGroupID: groupID,\n\t\tPeerID: peerID,\n\t}).Error\n\n\tif err != nil {\n\t\treturn status.Errorf(status.Internal, \"error adding peer to group 'All': %v\", err)\n\t}\n\n\treturn nil\n}\n\n// AddPeerToGroup adds a peer to a group\nfunc (s *SqlStore) AddPeerToGroup(ctx context.Context, accountID, peerID, groupID string) error {\n\tpeer := &types.GroupPeer{\n\t\tAccountID: accountID,\n\t\tGroupID: groupID,\n\t\tPeerID: peerID,\n\t}\n\n\terr := s.db.Clauses(clause.OnConflict{\n\t\tColumns: []clause.Column{{Name: \"group_id\"}, {Name: \"peer_id\"}},\n\t\tDoNothing: true,\n\t}).Create(peer).Error\n\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to add peer %s to group %s for account %s: %v\", peerID, groupID, accountID, err)\n\t\treturn status.Errorf(status.Internal, \"failed to add peer to group\")\n\t}\n\n\treturn nil\n}\n\n// RemovePeerFromGroup removes a peer from a group\nfunc (s *SqlStore) RemovePeerFromGroup(ctx context.Context, peerID string, groupID string) error {\n\terr := s.db.\n\t\tDelete(&types.GroupPeer{}, \"group_id = ? AND peer_id = ?\", groupID, peerID).Error\n\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to remove peer %s from group %s: %v\", peerID, groupID, err)\n\t\treturn status.Errorf(status.Internal, \"failed to remove peer from group\")\n\t}\n\n\treturn nil\n}\n\n// RemovePeerFromAllGroups removes a peer from all groups\nfunc (s *SqlStore) RemovePeerFromAllGroups(ctx context.Context, peerID string) error {\n\terr := s.db.\n\t\tDelete(&types.GroupPeer{}, \"peer_id = ?\", peerID).Error\n\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to remove peer %s from all groups: %v\", peerID, err)\n\t\treturn status.Errorf(status.Internal, \"failed to remove peer from all groups\")\n\t}\n\n\treturn nil\n}\n\n// AddResourceToGroup adds a resource to a group. Method always needs to run n a transaction\nfunc (s *SqlStore) AddResourceToGroup(ctx context.Context, accountId string, groupID string, resource *types.Resource) error {\n\tvar group types.Group\n\tresult := s.db.Where(accountAndIDQueryCondition, accountId, groupID).Take(&group)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn status.NewGroupNotFoundError(groupID)\n\t\t}\n\n\t\treturn status.Errorf(status.Internal, \"issue finding group: %s\", result.Error)\n\t}\n\n\tfor _, res := range group.Resources {\n\t\tif res.ID == resource.ID {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tgroup.Resources = append(group.Resources, *resource)\n\n\tif err := s.db.Save(&group).Error; err != nil {\n\t\treturn status.Errorf(status.Internal, \"issue updating group: %s\", err)\n\t}\n\n\treturn nil\n}\n\n// RemoveResourceFromGroup removes a resource from a group. Method always needs to run in a transaction\nfunc (s *SqlStore) RemoveResourceFromGroup(ctx context.Context, accountId string, groupID string, resourceID string) error {\n\tvar group types.Group\n\tresult := s.db.Where(accountAndIDQueryCondition, accountId, groupID).Take(&group)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn status.NewGroupNotFoundError(groupID)\n\t\t}\n\n\t\treturn status.Errorf(status.Internal, \"issue finding group: %s\", result.Error)\n\t}\n\n\tfor i, res := range group.Resources {\n\t\tif res.ID == resourceID {\n\t\t\tgroup.Resources = append(group.Resources[:i], group.Resources[i+1:]...)\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif err := s.db.Save(&group).Error; err != nil {\n\t\treturn status.Errorf(status.Internal, \"issue updating group: %s\", err)\n\t}\n\n\treturn nil\n}\n\n// GetPeerGroups retrieves all groups assigned to a specific peer in a given account.\nfunc (s *SqlStore) GetPeerGroups(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string) ([]*types.Group, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar groups []*types.Group\n\tquery := tx.\n\t\tJoins(\"JOIN group_peers ON group_peers.group_id = groups.id\").\n\t\tWhere(\"group_peers.peer_id = ?\", peerId).\n\t\tPreload(clause.Associations).\n\t\tFind(&groups)\n\n\tif query.Error != nil {\n\t\treturn nil, query.Error\n\t}\n\n\tfor _, group := range groups {\n\t\tgroup.LoadGroupPeers()\n\t}\n\n\treturn groups, nil\n}\n\n// GetPeerGroupIDs retrieves all group IDs assigned to a specific peer in a given account.\nfunc (s *SqlStore) GetPeerGroupIDs(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string) ([]string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar groupIDs []string\n\tquery := tx.\n\t\tModel(&types.GroupPeer{}).\n\t\tWhere(\"account_id = ? AND peer_id = ?\", accountId, peerId).\n\t\tPluck(\"group_id\", &groupIDs)\n\n\tif query.Error != nil {\n\t\tif errors.Is(query.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"no groups found for peer %s in account %s\", peerId, accountId)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get group IDs for peer %s in account %s: %v\", peerId, accountId, query.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get group IDs for peer from store\")\n\t}\n\n\treturn groupIDs, nil\n}\n\n// GetAccountPeers retrieves peers for an account.\nfunc (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) {\n\tvar peers []*nbpeer.Peer\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\tquery := tx.Where(accountIDCondition, accountID)\n\n\tif nameFilter != \"\" {\n\t\tquery = query.Where(\"name LIKE ?\", \"%\"+nameFilter+\"%\")\n\t}\n\tif ipFilter != \"\" {\n\t\tquery = query.Where(\"ip LIKE ?\", \"%\"+ipFilter+\"%\")\n\t}\n\n\tif err := query.Find(&peers).Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers from store\")\n\t}\n\n\treturn peers, nil\n}\n\n// GetUserPeers retrieves peers for a user.\nfunc (s *SqlStore) GetUserPeers(ctx context.Context, lockStrength LockingStrength, accountID, userID string) ([]*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peers []*nbpeer.Peer\n\n\t// Exclude peers added via setup keys, as they are not user-specific and have an empty user_id.\n\tif userID == \"\" {\n\t\treturn peers, nil\n\t}\n\n\tresult := tx.\n\t\tFind(&peers, \"account_id = ? AND user_id = ?\", accountID, userID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers from store\")\n\t}\n\n\treturn peers, nil\n}\n\nfunc (s *SqlStore) AddPeerToAccount(ctx context.Context, peer *nbpeer.Peer) error {\n\tif err := s.db.Create(peer).Error; err != nil {\n\t\treturn status.Errorf(status.Internal, \"issue adding peer to account: %s\", err)\n\t}\n\n\treturn nil\n}\n\n// GetPeerByID retrieves a peer by its ID and account ID.\nfunc (s *SqlStore) GetPeerByID(ctx context.Context, lockStrength LockingStrength, accountID, peerID string) (*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peer *nbpeer.Peer\n\tresult := tx.\n\t\tTake(&peer, accountAndIDQueryCondition, accountID, peerID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPeerNotFoundError(peerID)\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peer from store\")\n\t}\n\n\treturn peer, nil\n}\n\n// GetPeersByIDs retrieves peers by their IDs and account ID.\nfunc (s *SqlStore) GetPeersByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, peerIDs []string) (map[string]*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peers []*nbpeer.Peer\n\tresult := tx.Find(&peers, accountAndIDsQueryCondition, accountID, peerIDs)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers by ID's from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers by ID's from the store\")\n\t}\n\n\tpeersMap := make(map[string]*nbpeer.Peer)\n\tfor _, peer := range peers {\n\t\tpeersMap[peer.ID] = peer\n\t}\n\n\treturn peersMap, nil\n}\n\n// GetAccountPeersWithExpiration retrieves a list of peers that have login expiration enabled and added by a user.\nfunc (s *SqlStore) GetAccountPeersWithExpiration(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peers []*nbpeer.Peer\n\tresult := tx.\n\t\tWhere(\"login_expiration_enabled = ? AND user_id IS NOT NULL AND user_id != ''\", true).\n\t\tFind(&peers, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers with expiration from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers with expiration from store\")\n\t}\n\n\treturn peers, nil\n}\n\n// GetAccountPeersWithInactivity retrieves a list of peers that have login expiration enabled and added by a user.\nfunc (s *SqlStore) GetAccountPeersWithInactivity(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peers []*nbpeer.Peer\n\tresult := tx.\n\t\tWhere(\"inactivity_expiration_enabled = ? AND user_id IS NOT NULL AND user_id != ''\", true).\n\t\tFind(&peers, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers with inactivity from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers with inactivity from store\")\n\t}\n\n\treturn peers, nil\n}\n\n// GetAllEphemeralPeers retrieves all peers with Ephemeral set to true across all accounts, optimized for batch processing.\nfunc (s *SqlStore) GetAllEphemeralPeers(ctx context.Context, lockStrength LockingStrength) ([]*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar allEphemeralPeers, batchPeers []*nbpeer.Peer\n\tresult := tx.\n\t\tWhere(\"ephemeral = ?\", true).\n\t\tFindInBatches(&batchPeers, 1000, func(tx *gorm.DB, batch int) error {\n\t\t\tallEphemeralPeers = append(allEphemeralPeers, batchPeers...)\n\t\t\treturn nil\n\t\t})\n\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to retrieve ephemeral peers: %s\", result.Error)\n\t\treturn nil, fmt.Errorf(\"failed to retrieve ephemeral peers\")\n\t}\n\n\treturn allEphemeralPeers, nil\n}\n\n// DeletePeer removes a peer from the store.\nfunc (s *SqlStore) DeletePeer(ctx context.Context, accountID string, peerID string) error {\n\tresult := s.db.Delete(&nbpeer.Peer{}, accountAndIDQueryCondition, accountID, peerID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete peer from the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to delete peer from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewPeerNotFoundError(peerID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) IncrementNetworkSerial(ctx context.Context, accountId string) error {\n\tresult := s.db.Model(&types.Account{}).Where(idQueryCondition, accountId).Update(\"network_serial\", gorm.Expr(\"network_serial + 1\"))\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to increment network serial count in store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to increment network serial count in store\")\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) ExecuteInTransaction(ctx context.Context, operation func(store Store) error) error {\n\ttimeoutCtx, cancel := context.WithTimeout(context.Background(), s.transactionTimeout)\n\tdefer cancel()\n\n\tstartTime := time.Now()\n\ttx := s.db.WithContext(timeoutCtx).Begin()\n\tif tx.Error != nil {\n\t\treturn tx.Error\n\t}\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\ttx.Rollback()\n\t\t\tpanic(r)\n\t\t}\n\t}()\n\n\tif s.storeEngine == types.PostgresStoreEngine {\n\t\tif err := tx.Exec(\"SET LOCAL statement_timeout = '1min'\").Error; err != nil {\n\t\t\ttx.Rollback()\n\t\t\treturn fmt.Errorf(\"failed to set statement timeout: %w\", err)\n\t\t}\n\t\tif err := tx.Exec(\"SET LOCAL lock_timeout = '1min'\").Error; err != nil {\n\t\t\ttx.Rollback()\n\t\t\treturn fmt.Errorf(\"failed to set lock timeout: %w\", err)\n\t\t}\n\t}\n\n\t// For MySQL, disable FK checks within this transaction to avoid deadlocks\n\t// This is session-scoped and doesn't require SUPER privileges\n\tif s.storeEngine == types.MysqlStoreEngine {\n\t\tif err := tx.Exec(\"SET FOREIGN_KEY_CHECKS = 0\").Error; err != nil {\n\t\t\ttx.Rollback()\n\t\t\treturn fmt.Errorf(\"failed to disable FK checks: %w\", err)\n\t\t}\n\t}\n\n\trepo := s.withTx(tx)\n\terr := operation(repo)\n\tif err != nil {\n\t\ttx.Rollback()\n\t\tif errors.Is(err, context.DeadlineExceeded) || errors.Is(timeoutCtx.Err(), context.DeadlineExceeded) {\n\t\t\tlog.WithContext(ctx).Warnf(\"transaction exceeded %s timeout after %v, stack: %s\", s.transactionTimeout, time.Since(startTime), debug.Stack())\n\t\t}\n\t\treturn err\n\t}\n\n\t// Re-enable FK checks before commit (optional, as transaction end resets it)\n\tif s.storeEngine == types.MysqlStoreEngine {\n\t\tif err := tx.Exec(\"SET FOREIGN_KEY_CHECKS = 1\").Error; err != nil {\n\t\t\ttx.Rollback()\n\t\t\treturn fmt.Errorf(\"failed to re-enable FK checks: %w\", err)\n\t\t}\n\t}\n\n\terr = tx.Commit().Error\n\tif err != nil {\n\t\tif errors.Is(err, context.DeadlineExceeded) || errors.Is(timeoutCtx.Err(), context.DeadlineExceeded) {\n\t\t\tlog.WithContext(ctx).Warnf(\"transaction commit exceeded %s timeout after %v, stack: %s\", s.transactionTimeout, time.Since(startTime), debug.Stack())\n\t\t}\n\t\treturn err\n\t}\n\n\tlog.WithContext(ctx).Tracef(\"transaction took %v\", time.Since(startTime))\n\tif s.metrics != nil {\n\t\ts.metrics.StoreMetrics().CountTransactionDuration(time.Since(startTime))\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) withTx(tx *gorm.DB) Store {\n\treturn &SqlStore{\n\t\tdb: tx,\n\t\tstoreEngine: s.storeEngine,\n\t\tfieldEncrypt: s.fieldEncrypt,\n\t}\n}\n\n// transaction wraps a GORM transaction with MySQL-specific FK checks handling\n// Use this instead of db.Transaction() directly to avoid deadlocks on MySQL/Aurora\nfunc (s *SqlStore) transaction(fn func(*gorm.DB) error) error {\n\treturn s.db.Transaction(func(tx *gorm.DB) error {\n\t\t// For MySQL, disable FK checks within this transaction to avoid deadlocks\n\t\t// This is session-scoped and doesn't require SUPER privileges\n\t\tif s.storeEngine == types.MysqlStoreEngine {\n\t\t\tif err := tx.Exec(\"SET FOREIGN_KEY_CHECKS = 0\").Error; err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to disable FK checks: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\terr := fn(tx)\n\n\t\t// Re-enable FK checks before commit (optional, as transaction end resets it)\n\t\tif s.storeEngine == types.MysqlStoreEngine && err == nil {\n\t\t\tif fkErr := tx.Exec(\"SET FOREIGN_KEY_CHECKS = 1\").Error; fkErr != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to re-enable FK checks: %w\", fkErr)\n\t\t\t}\n\t\t}\n\n\t\treturn err\n\t})\n}\n\nfunc (s *SqlStore) GetDB() *gorm.DB {\n\treturn s.db\n}\n\n// SetFieldEncrypt sets the field encryptor for encrypting sensitive user data.\nfunc (s *SqlStore) SetFieldEncrypt(enc *crypt.FieldEncrypt) {\n\ts.fieldEncrypt = enc\n}\n\nfunc (s *SqlStore) GetAccountDNSSettings(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.DNSSettings, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountDNSSettings types.AccountDNSSettings\n\tresult := tx.Model(&types.Account{}).\n\t\tTake(&accountDNSSettings, idQueryCondition, accountID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewAccountNotFoundError(accountID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get dns settings from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get dns settings from store\")\n\t}\n\treturn &accountDNSSettings.DNSSettings, nil\n}\n\n// AccountExists checks whether an account exists by the given ID.\nfunc (s *SqlStore) AccountExists(ctx context.Context, lockStrength LockingStrength, id string) (bool, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar accountID string\n\tresult := tx.Model(&types.Account{}).\n\t\tSelect(\"id\").Take(&accountID, idQueryCondition, id)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, result.Error\n\t}\n\n\treturn accountID != \"\", nil\n}\n\n// GetAccountDomainAndCategory retrieves the Domain and DomainCategory fields for an account based on the given accountID.\nfunc (s *SqlStore) GetAccountDomainAndCategory(ctx context.Context, lockStrength LockingStrength, accountID string) (string, string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar account types.Account\n\tresult := tx.Model(&types.Account{}).Select(\"domain\", \"domain_category\").\n\t\tWhere(idQueryCondition, accountID).Take(&account)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", \"\", status.Errorf(status.NotFound, \"account not found\")\n\t\t}\n\t\treturn \"\", \"\", status.Errorf(status.Internal, \"failed to get domain category from store: %v\", result.Error)\n\t}\n\n\treturn account.Domain, account.DomainCategory, nil\n}\n\n// GetGroupByID retrieves a group by ID and account ID.\nfunc (s *SqlStore) GetGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) (*types.Group, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar group *types.Group\n\tresult := tx.Preload(clause.Associations).Take(&group, accountAndIDQueryCondition, accountID, groupID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewGroupNotFoundError(groupID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get group from store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get group from store\")\n\t}\n\n\tgroup.LoadGroupPeers()\n\n\treturn group, nil\n}\n\n// GetGroupByName retrieves a group by name and account ID.\nfunc (s *SqlStore) GetGroupByName(ctx context.Context, lockStrength LockingStrength, accountID, groupName string) (*types.Group, error) {\n\ttx := s.db\n\n\tvar group types.Group\n\n\t// TODO: This fix is accepted for now, but if we need to handle this more frequently\n\t// we may need to reconsider changing the types.\n\tquery := tx.Preload(clause.Associations)\n\n\tresult := query.\n\t\tModel(&types.Group{}).\n\t\tJoins(\"LEFT JOIN group_peers ON group_peers.group_id = groups.id\").\n\t\tWhere(\"groups.account_id = ? AND groups.name = ?\", accountID, groupName).\n\t\tGroup(\"groups.id\").\n\t\tOrder(\"COUNT(group_peers.peer_id) DESC\").\n\t\tLimit(1).\n\t\tFirst(&group)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewGroupNotFoundError(groupName)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get group by name from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get group by name from store\")\n\t}\n\n\tgroup.LoadGroupPeers()\n\n\treturn &group, nil\n}\n\n// GetGroupsByIDs retrieves groups by their IDs and account ID.\nfunc (s *SqlStore) GetGroupsByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, groupIDs []string) (map[string]*types.Group, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar groups []*types.Group\n\tresult := tx.Preload(clause.Associations).Find(&groups, accountAndIDsQueryCondition, accountID, groupIDs)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get groups by ID's from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get groups by ID's from store\")\n\t}\n\n\tgroupsMap := make(map[string]*types.Group)\n\tfor _, group := range groups {\n\t\tgroup.LoadGroupPeers()\n\t\tgroupsMap[group.ID] = group\n\t}\n\n\treturn groupsMap, nil\n}\n\n// CreateGroup creates a group in the store.\nfunc (s *SqlStore) CreateGroup(ctx context.Context, group *types.Group) error {\n\tif group == nil {\n\t\treturn status.Errorf(status.InvalidArgument, \"group is nil\")\n\t}\n\n\tif err := s.db.Omit(clause.Associations).Create(group).Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save group to store: %v\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save group to store\")\n\t}\n\n\treturn nil\n}\n\n// UpdateGroup updates a group in the store.\nfunc (s *SqlStore) UpdateGroup(ctx context.Context, group *types.Group) error {\n\tif group == nil {\n\t\treturn status.Errorf(status.InvalidArgument, \"group is nil\")\n\t}\n\n\tif err := s.db.Omit(clause.Associations).Save(group).Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save group to store: %v\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save group to store\")\n\t}\n\n\treturn nil\n}\n\n// DeleteGroup deletes a group from the database.\nfunc (s *SqlStore) DeleteGroup(ctx context.Context, accountID, groupID string) error {\n\tresult := s.db.Select(clause.Associations).\n\t\tDelete(&types.Group{}, accountAndIDQueryCondition, accountID, groupID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete group from store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete group from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewGroupNotFoundError(groupID)\n\t}\n\n\treturn nil\n}\n\n// DeleteGroups deletes groups from the database.\nfunc (s *SqlStore) DeleteGroups(ctx context.Context, accountID string, groupIDs []string) error {\n\tresult := s.db.Select(clause.Associations).\n\t\tDelete(&types.Group{}, accountAndIDsQueryCondition, accountID, groupIDs)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete groups from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete groups from store\")\n\t}\n\n\treturn nil\n}\n\n// GetAccountPolicies retrieves policies for an account.\nfunc (s *SqlStore) GetAccountPolicies(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.Policy, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar policies []*types.Policy\n\tresult := tx.\n\t\tPreload(clause.Associations).Find(&policies, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get policies from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get policies from store\")\n\t}\n\n\treturn policies, nil\n}\n\n// GetPolicyByID retrieves a policy by its ID and account ID.\nfunc (s *SqlStore) GetPolicyByID(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) (*types.Policy, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar policy *types.Policy\n\n\tresult := tx.Preload(clause.Associations).\n\t\tTake(&policy, accountAndIDQueryCondition, accountID, policyID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPolicyNotFoundError(policyID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get policy from store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get policy from store\")\n\t}\n\n\treturn policy, nil\n}\n\nfunc (s *SqlStore) CreatePolicy(ctx context.Context, policy *types.Policy) error {\n\tresult := s.db.Create(policy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create policy in store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create policy in store\")\n\t}\n\n\treturn nil\n}\n\n// SavePolicy saves a policy to the database.\nfunc (s *SqlStore) SavePolicy(ctx context.Context, policy *types.Policy) error {\n\tresult := s.db.Session(&gorm.Session{FullSaveAssociations: true}).Save(policy)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save policy to the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save policy to store\")\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) DeletePolicy(ctx context.Context, accountID, policyID string) error {\n\treturn s.transaction(func(tx *gorm.DB) error {\n\t\tif err := tx.Where(\"policy_id = ?\", policyID).Delete(&types.PolicyRule{}).Error; err != nil {\n\t\t\treturn fmt.Errorf(\"delete policy rules: %w\", err)\n\t\t}\n\n\t\tresult := tx.\n\t\t\tWhere(accountAndIDQueryCondition, accountID, policyID).\n\t\t\tDelete(&types.Policy{})\n\n\t\tif err := result.Error; err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"failed to delete policy from store: %s\", err)\n\t\t\treturn status.Errorf(status.Internal, \"failed to delete policy from store\")\n\t\t}\n\n\t\tif result.RowsAffected == 0 {\n\t\t\treturn status.NewPolicyNotFoundError(policyID)\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\nfunc (s *SqlStore) GetPolicyRulesByResourceID(ctx context.Context, lockStrength LockingStrength, accountID string, resourceID string) ([]*types.PolicyRule, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar policyRules []*types.PolicyRule\n\tresourceIDPattern := `%\"ID\":\"` + resourceID + `\"%`\n\tresult := tx.Where(\"source_resource LIKE ? OR destination_resource LIKE ?\", resourceIDPattern, resourceIDPattern).\n\t\tFind(&policyRules)\n\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get policy rules for resource id from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get policy rules for resource id from store\")\n\t}\n\n\treturn policyRules, nil\n}\n\n// GetAccountPostureChecks retrieves posture checks for an account.\nfunc (s *SqlStore) GetAccountPostureChecks(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*posture.Checks, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar postureChecks []*posture.Checks\n\tresult := tx.Find(&postureChecks, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get posture checks from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get posture checks from store\")\n\t}\n\n\treturn postureChecks, nil\n}\n\n// GetPostureChecksByID retrieves posture checks by their ID and account ID.\nfunc (s *SqlStore) GetPostureChecksByID(ctx context.Context, lockStrength LockingStrength, accountID, postureChecksID string) (*posture.Checks, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar postureCheck *posture.Checks\n\tresult := tx.\n\t\tTake(&postureCheck, accountAndIDQueryCondition, accountID, postureChecksID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPostureChecksNotFoundError(postureChecksID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get posture check from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get posture check from store\")\n\t}\n\n\treturn postureCheck, nil\n}\n\n// GetPostureChecksByIDs retrieves posture checks by their IDs and account ID.\nfunc (s *SqlStore) GetPostureChecksByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, postureChecksIDs []string) (map[string]*posture.Checks, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar postureChecks []*posture.Checks\n\tresult := tx.Find(&postureChecks, accountAndIDsQueryCondition, accountID, postureChecksIDs)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get posture checks by ID's from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get posture checks by ID's from store\")\n\t}\n\n\tpostureChecksMap := make(map[string]*posture.Checks)\n\tfor _, postureCheck := range postureChecks {\n\t\tpostureChecksMap[postureCheck.ID] = postureCheck\n\t}\n\n\treturn postureChecksMap, nil\n}\n\n// SavePostureChecks saves a posture checks to the database.\nfunc (s *SqlStore) SavePostureChecks(ctx context.Context, postureCheck *posture.Checks) error {\n\tresult := s.db.Save(postureCheck)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save posture checks to store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save posture checks to store\")\n\t}\n\n\treturn nil\n}\n\n// DeletePostureChecks deletes a posture checks from the database.\nfunc (s *SqlStore) DeletePostureChecks(ctx context.Context, accountID, postureChecksID string) error {\n\tresult := s.db.Delete(&posture.Checks{}, accountAndIDQueryCondition, accountID, postureChecksID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete posture checks from store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete posture checks from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewPostureChecksNotFoundError(postureChecksID)\n\t}\n\n\treturn nil\n}\n\n// GetAccountRoutes retrieves network routes for an account.\nfunc (s *SqlStore) GetAccountRoutes(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*route.Route, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar routes []*route.Route\n\tresult := tx.Find(&routes, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get routes from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get routes from store\")\n\t}\n\n\treturn routes, nil\n}\n\n// GetRouteByID retrieves a route by its ID and account ID.\nfunc (s *SqlStore) GetRouteByID(ctx context.Context, lockStrength LockingStrength, accountID string, routeID string) (*route.Route, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar route *route.Route\n\tresult := tx.Take(&route, accountAndIDQueryCondition, accountID, routeID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewRouteNotFoundError(routeID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get route from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get route from store\")\n\t}\n\n\treturn route, nil\n}\n\n// SaveRoute saves a route to the database.\nfunc (s *SqlStore) SaveRoute(ctx context.Context, route *route.Route) error {\n\tresult := s.db.Save(route)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save route to the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save route to store\")\n\t}\n\n\treturn nil\n}\n\n// DeleteRoute deletes a route from the database.\nfunc (s *SqlStore) DeleteRoute(ctx context.Context, accountID, routeID string) error {\n\tresult := s.db.Delete(&route.Route{}, accountAndIDQueryCondition, accountID, routeID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete route from the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to delete route from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewRouteNotFoundError(routeID)\n\t}\n\n\treturn nil\n}\n\n// GetAccountSetupKeys retrieves setup keys for an account.\nfunc (s *SqlStore) GetAccountSetupKeys(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.SetupKey, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar setupKeys []*types.SetupKey\n\tresult := tx.\n\t\tFind(&setupKeys, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get setup keys from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get setup keys from store\")\n\t}\n\n\treturn setupKeys, nil\n}\n\n// GetSetupKeyByID retrieves a setup key by its ID and account ID.\nfunc (s *SqlStore) GetSetupKeyByID(ctx context.Context, lockStrength LockingStrength, accountID, setupKeyID string) (*types.SetupKey, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar setupKey *types.SetupKey\n\tresult := tx.Take(&setupKey, accountAndIDQueryCondition, accountID, setupKeyID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewSetupKeyNotFoundError(setupKeyID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get setup key from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get setup key from store\")\n\t}\n\n\treturn setupKey, nil\n}\n\n// SaveSetupKey saves a setup key to the database.\nfunc (s *SqlStore) SaveSetupKey(ctx context.Context, setupKey *types.SetupKey) error {\n\tresult := s.db.Save(setupKey)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save setup key to store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save setup key to store\")\n\t}\n\n\treturn nil\n}\n\n// DeleteSetupKey deletes a setup key from the database.\nfunc (s *SqlStore) DeleteSetupKey(ctx context.Context, accountID, keyID string) error {\n\tresult := s.db.Delete(&types.SetupKey{}, accountAndIDQueryCondition, accountID, keyID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete setup key from store: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete setup key from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewSetupKeyNotFoundError(keyID)\n\t}\n\n\treturn nil\n}\n\n// GetAccountNameServerGroups retrieves name server groups for an account.\nfunc (s *SqlStore) GetAccountNameServerGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbdns.NameServerGroup, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar nsGroups []*nbdns.NameServerGroup\n\tresult := tx.Find(&nsGroups, accountIDCondition, accountID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get name server groups from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get name server groups from store\")\n\t}\n\n\treturn nsGroups, nil\n}\n\n// GetNameServerGroupByID retrieves a name server group by its ID and account ID.\nfunc (s *SqlStore) GetNameServerGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, nsGroupID string) (*nbdns.NameServerGroup, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar nsGroup *nbdns.NameServerGroup\n\tresult := tx.\n\t\tTake(&nsGroup, accountAndIDQueryCondition, accountID, nsGroupID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewNameServerGroupNotFoundError(nsGroupID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get name server group from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get name server group from store\")\n\t}\n\n\treturn nsGroup, nil\n}\n\n// SaveNameServerGroup saves a name server group to the database.\nfunc (s *SqlStore) SaveNameServerGroup(ctx context.Context, nameServerGroup *nbdns.NameServerGroup) error {\n\tresult := s.db.Save(nameServerGroup)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save name server group to the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save name server group to store\")\n\t}\n\treturn nil\n}\n\n// DeleteNameServerGroup deletes a name server group from the database.\nfunc (s *SqlStore) DeleteNameServerGroup(ctx context.Context, accountID, nsGroupID string) error {\n\tresult := s.db.Delete(&nbdns.NameServerGroup{}, accountAndIDQueryCondition, accountID, nsGroupID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete name server group from the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to delete name server group from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewNameServerGroupNotFoundError(nsGroupID)\n\t}\n\n\treturn nil\n}\n\n// SaveDNSSettings saves the DNS settings to the store.\nfunc (s *SqlStore) SaveDNSSettings(ctx context.Context, accountID string, settings *types.DNSSettings) error {\n\tresult := s.db.Model(&types.Account{}).\n\t\tWhere(idQueryCondition, accountID).Updates(&types.AccountDNSSettings{DNSSettings: *settings})\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save dns settings to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save dns settings to store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewAccountNotFoundError(accountID)\n\t}\n\n\treturn nil\n}\n\n// SaveAccountSettings stores the account settings in DB.\nfunc (s *SqlStore) SaveAccountSettings(ctx context.Context, accountID string, settings *types.Settings) error {\n\tresult := s.db.Model(&types.Account{}).\n\t\tSelect(\"*\").Where(idQueryCondition, accountID).Updates(&types.AccountSettings{Settings: settings})\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save account settings to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save account settings to store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewAccountNotFoundError(accountID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetAccountNetworks(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*networkTypes.Network, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar networks []*networkTypes.Network\n\tresult := tx.Find(&networks, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get networks from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get networks from store\")\n\t}\n\n\treturn networks, nil\n}\n\nfunc (s *SqlStore) GetNetworkByID(ctx context.Context, lockStrength LockingStrength, accountID, networkID string) (*networkTypes.Network, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar network *networkTypes.Network\n\tresult := tx.Take(&network, accountAndIDQueryCondition, accountID, networkID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewNetworkNotFoundError(networkID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network from store\")\n\t}\n\n\treturn network, nil\n}\n\nfunc (s *SqlStore) SaveNetwork(ctx context.Context, network *networkTypes.Network) error {\n\tresult := s.db.Save(network)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save network to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save network to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteNetwork(ctx context.Context, accountID, networkID string) error {\n\tresult := s.db.Delete(&networkTypes.Network{}, accountAndIDQueryCondition, accountID, networkID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete network from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete network from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewNetworkNotFoundError(networkID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetNetworkRoutersByNetID(ctx context.Context, lockStrength LockingStrength, accountID, netID string) ([]*routerTypes.NetworkRouter, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netRouters []*routerTypes.NetworkRouter\n\tresult := tx.\n\t\tFind(&netRouters, \"account_id = ? AND network_id = ?\", accountID, netID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network routers from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network routers from store\")\n\t}\n\n\treturn netRouters, nil\n}\n\nfunc (s *SqlStore) GetNetworkRoutersByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*routerTypes.NetworkRouter, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netRouters []*routerTypes.NetworkRouter\n\tresult := tx.\n\t\tFind(&netRouters, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network routers from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network routers from store\")\n\t}\n\n\treturn netRouters, nil\n}\n\nfunc (s *SqlStore) GetNetworkRouterByID(ctx context.Context, lockStrength LockingStrength, accountID, routerID string) (*routerTypes.NetworkRouter, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netRouter *routerTypes.NetworkRouter\n\tresult := tx.\n\t\tTake(&netRouter, accountAndIDQueryCondition, accountID, routerID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewNetworkRouterNotFoundError(routerID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network router from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network router from store\")\n\t}\n\n\treturn netRouter, nil\n}\n\nfunc (s *SqlStore) SaveNetworkRouter(ctx context.Context, router *routerTypes.NetworkRouter) error {\n\tresult := s.db.Save(router)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save network router to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save network router to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteNetworkRouter(ctx context.Context, accountID, routerID string) error {\n\tresult := s.db.Delete(&routerTypes.NetworkRouter{}, accountAndIDQueryCondition, accountID, routerID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete network router from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete network router from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewNetworkRouterNotFoundError(routerID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetNetworkResourcesByNetID(ctx context.Context, lockStrength LockingStrength, accountID, networkID string) ([]*resourceTypes.NetworkResource, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netResources []*resourceTypes.NetworkResource\n\tresult := tx.\n\t\tFind(&netResources, \"account_id = ? AND network_id = ?\", accountID, networkID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network resources from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network resources from store\")\n\t}\n\n\treturn netResources, nil\n}\n\nfunc (s *SqlStore) GetNetworkResourcesByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*resourceTypes.NetworkResource, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netResources []*resourceTypes.NetworkResource\n\tresult := tx.\n\t\tFind(&netResources, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network resources from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network resources from store\")\n\t}\n\n\treturn netResources, nil\n}\n\nfunc (s *SqlStore) GetNetworkResourceByID(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) (*resourceTypes.NetworkResource, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netResources *resourceTypes.NetworkResource\n\tresult := tx.\n\t\tTake(&netResources, accountAndIDQueryCondition, accountID, resourceID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewNetworkResourceNotFoundError(resourceID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network resource from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network resource from store\")\n\t}\n\n\treturn netResources, nil\n}\n\nfunc (s *SqlStore) GetNetworkResourceByName(ctx context.Context, lockStrength LockingStrength, accountID, resourceName string) (*resourceTypes.NetworkResource, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar netResources *resourceTypes.NetworkResource\n\tresult := tx.\n\t\tTake(&netResources, \"account_id = ? AND name = ?\", accountID, resourceName)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewNetworkResourceNotFoundError(resourceName)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get network resource from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get network resource from store\")\n\t}\n\n\treturn netResources, nil\n}\n\nfunc (s *SqlStore) SaveNetworkResource(ctx context.Context, resource *resourceTypes.NetworkResource) error {\n\tresult := s.db.Save(resource)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save network resource to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to save network resource to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteNetworkResource(ctx context.Context, accountID, resourceID string) error {\n\tresult := s.db.Delete(&resourceTypes.NetworkResource{}, accountAndIDQueryCondition, accountID, resourceID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete network resource from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete network resource from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewNetworkResourceNotFoundError(resourceID)\n\t}\n\n\treturn nil\n}\n\n// GetPATByHashedToken returns a PersonalAccessToken by its hashed token.\nfunc (s *SqlStore) GetPATByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken string) (*types.PersonalAccessToken, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar pat types.PersonalAccessToken\n\tresult := tx.Take(&pat, \"hashed_token = ?\", hashedToken)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPATNotFoundError(hashedToken)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get pat by hash from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get pat by hash from store\")\n\t}\n\n\treturn &pat, nil\n}\n\n// GetPATByID retrieves a personal access token by its ID and user ID.\nfunc (s *SqlStore) GetPATByID(ctx context.Context, lockStrength LockingStrength, userID string, patID string) (*types.PersonalAccessToken, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar pat types.PersonalAccessToken\n\tresult := tx.\n\t\tTake(&pat, \"id = ? AND user_id = ?\", patID, userID)\n\tif err := result.Error; err != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewPATNotFoundError(patID)\n\t\t}\n\t\tlog.WithContext(ctx).Errorf(\"failed to get pat from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get pat from store\")\n\t}\n\n\treturn &pat, nil\n}\n\n// GetUserPATs retrieves personal access tokens for a user.\nfunc (s *SqlStore) GetUserPATs(ctx context.Context, lockStrength LockingStrength, userID string) ([]*types.PersonalAccessToken, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar pats []*types.PersonalAccessToken\n\tresult := tx.Find(&pats, \"user_id = ?\", userID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get user pat's from the store: %s\", err)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get user pat's from store\")\n\t}\n\n\treturn pats, nil\n}\n\n// MarkPATUsed marks a personal access token as used.\nfunc (s *SqlStore) MarkPATUsed(ctx context.Context, patID string) error {\n\tpatCopy := types.PersonalAccessToken{\n\t\tLastUsed: util.ToPtr(time.Now().UTC()),\n\t}\n\n\tfieldsToUpdate := []string{\"last_used\"}\n\tresult := s.db.Select(fieldsToUpdate).\n\t\tWhere(idQueryCondition, patID).Updates(&patCopy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to mark pat as used: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to mark pat as used\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewPATNotFoundError(patID)\n\t}\n\n\treturn nil\n}\n\n// SavePAT saves a personal access token to the database.\nfunc (s *SqlStore) SavePAT(ctx context.Context, pat *types.PersonalAccessToken) error {\n\tresult := s.db.Save(pat)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to save pat to the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to save pat to store\")\n\t}\n\n\treturn nil\n}\n\n// DeletePAT deletes a personal access token from the database.\nfunc (s *SqlStore) DeletePAT(ctx context.Context, userID, patID string) error {\n\tresult := s.db.Delete(&types.PersonalAccessToken{}, \"user_id = ? AND id = ?\", userID, patID)\n\tif err := result.Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete pat from the store: %s\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to delete pat from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewPATNotFoundError(patID)\n\t}\n\n\treturn nil\n}\n\n// GetProxyAccessTokenByHashedToken retrieves a proxy access token by its hashed value.\nfunc (s *SqlStore) GetProxyAccessTokenByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken types.HashedProxyToken) (*types.ProxyAccessToken, error) {\n\ttx := s.db.WithContext(ctx)\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar token types.ProxyAccessToken\n\tresult := tx.Take(&token, \"hashed_token = ?\", hashedToken)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"proxy access token not found\")\n\t\t}\n\t\treturn nil, status.Errorf(status.Internal, \"get proxy access token: %v\", result.Error)\n\t}\n\n\treturn &token, nil\n}\n\n// GetAllProxyAccessTokens retrieves all proxy access tokens.\nfunc (s *SqlStore) GetAllProxyAccessTokens(ctx context.Context, lockStrength LockingStrength) ([]*types.ProxyAccessToken, error) {\n\ttx := s.db.WithContext(ctx)\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar tokens []*types.ProxyAccessToken\n\tresult := tx.Find(&tokens)\n\tif result.Error != nil {\n\t\treturn nil, status.Errorf(status.Internal, \"get proxy access tokens: %v\", result.Error)\n\t}\n\n\treturn tokens, nil\n}\n\n// SaveProxyAccessToken saves a proxy access token to the database.\nfunc (s *SqlStore) SaveProxyAccessToken(ctx context.Context, token *types.ProxyAccessToken) error {\n\tif result := s.db.WithContext(ctx).Create(token); result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"save proxy access token: %v\", result.Error)\n\t}\n\treturn nil\n}\n\n// RevokeProxyAccessToken revokes a proxy access token by its ID.\nfunc (s *SqlStore) RevokeProxyAccessToken(ctx context.Context, tokenID string) error {\n\tresult := s.db.WithContext(ctx).Model(&types.ProxyAccessToken{}).Where(idQueryCondition, tokenID).Update(\"revoked\", true)\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"revoke proxy access token: %v\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"proxy access token not found\")\n\t}\n\n\treturn nil\n}\n\n// MarkProxyAccessTokenUsed updates the last used timestamp for a proxy access token.\nfunc (s *SqlStore) MarkProxyAccessTokenUsed(ctx context.Context, tokenID string) error {\n\tresult := s.db.WithContext(ctx).Model(&types.ProxyAccessToken{}).\n\t\tWhere(idQueryCondition, tokenID).\n\t\tUpdate(\"last_used\", time.Now().UTC())\n\tif result.Error != nil {\n\t\treturn status.Errorf(status.Internal, \"mark proxy access token as used: %v\", result.Error)\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"proxy access token not found\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetPeerByIP(ctx context.Context, lockStrength LockingStrength, accountID string, ip net.IP) (*nbpeer.Peer, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tjsonValue := fmt.Sprintf(`\"%s\"`, ip.String())\n\n\tvar peer nbpeer.Peer\n\tresult := tx.\n\t\tTake(&peer, \"account_id = ? AND ip = ?\", accountID, jsonValue)\n\tif result.Error != nil {\n\t\t// no logging here\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peer from store\")\n\t}\n\n\treturn &peer, nil\n}\n\nfunc (s *SqlStore) GetPeerIdByLabel(ctx context.Context, lockStrength LockingStrength, accountID string, hostname string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peerID string\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tSelect(\"id\").\n\t\t// Where(\" = ?\", hostname).\n\t\tWhere(\"account_id = ? AND dns_label = ?\", accountID, hostname).\n\t\tLimit(1).\n\t\tScan(&peerID)\n\n\tif peerID == \"\" {\n\t\treturn \"\", gorm.ErrRecordNotFound\n\t}\n\n\treturn peerID, result.Error\n}\n\nfunc (s *SqlStore) CountAccountsByPrivateDomain(ctx context.Context, domain string) (int64, error) {\n\tvar count int64\n\tresult := s.db.Model(&types.Account{}).\n\t\tWhere(\"domain = ? AND domain_category = ?\",\n\t\t\tstrings.ToLower(domain), types.PrivateCategory,\n\t\t).Count(&count)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to count accounts by private domain %s: %s\", domain, result.Error)\n\t\treturn 0, status.Errorf(status.Internal, \"failed to count accounts by private domain\")\n\t}\n\n\treturn count, nil\n}\n\nfunc (s *SqlStore) GetAccountGroupPeers(ctx context.Context, lockStrength LockingStrength, accountID string) (map[string]map[string]struct{}, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peers []types.GroupPeer\n\tresult := tx.Find(&peers, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get account group peers from store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get account group peers from store\")\n\t}\n\n\tgroupPeers := make(map[string]map[string]struct{})\n\tfor _, peer := range peers {\n\t\tif _, exists := groupPeers[peer.GroupID]; !exists {\n\t\t\tgroupPeers[peer.GroupID] = make(map[string]struct{})\n\t\t}\n\t\tgroupPeers[peer.GroupID][peer.PeerID] = struct{}{}\n\t}\n\n\treturn groupPeers, nil\n}\n\nfunc (s *SqlStore) IsPrimaryAccount(ctx context.Context, accountID string) (bool, string, error) {\n\tvar info types.PrimaryAccountInfo\n\tresult := s.db.Model(&types.Account{}).\n\t\tSelect(\"is_domain_primary_account, domain\").\n\t\tWhere(idQueryCondition, accountID).\n\t\tTake(&info)\n\n\tif result.Error != nil {\n\t\treturn false, \"\", status.Errorf(status.Internal, \"failed to get account info: %v\", result.Error)\n\t}\n\n\treturn info.IsDomainPrimaryAccount, info.Domain, nil\n}\n\nfunc (s *SqlStore) MarkAccountPrimary(ctx context.Context, accountID string) error {\n\tresult := s.db.Model(&types.Account{}).\n\t\tWhere(idQueryCondition, accountID).\n\t\tUpdate(\"is_domain_primary_account\", true)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to mark account as primary: %s\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to mark account as primary\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewAccountNotFoundError(accountID)\n\t}\n\n\treturn nil\n}\n\ntype accountNetworkPatch struct {\n\tNetwork *types.Network `gorm:\"embedded;embeddedPrefix:network_\"`\n}\n\nfunc (s *SqlStore) UpdateAccountNetwork(ctx context.Context, accountID string, ipNet net.IPNet) error {\n\tpatch := accountNetworkPatch{\n\t\tNetwork: &types.Network{Net: ipNet},\n\t}\n\n\tresult := s.db.\n\t\tModel(&types.Account{}).\n\t\tWhere(idQueryCondition, accountID).\n\t\tUpdates(&patch)\n\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update account network: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to update account network\")\n\t}\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewAccountNotFoundError(accountID)\n\t}\n\treturn nil\n}\n\nfunc (s *SqlStore) GetPeersByGroupIDs(ctx context.Context, accountID string, groupIDs []string) ([]*nbpeer.Peer, error) {\n\tif len(groupIDs) == 0 {\n\t\treturn []*nbpeer.Peer{}, nil\n\t}\n\n\tvar peers []*nbpeer.Peer\n\tpeerIDsSubquery := s.db.Model(&types.GroupPeer{}).\n\t\tSelect(\"DISTINCT peer_id\").\n\t\tWhere(\"account_id = ? AND group_id IN ?\", accountID, groupIDs)\n\n\tresult := s.db.Where(\"id IN (?)\", peerIDsSubquery).Find(&peers)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peers by group IDs: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get peers by group IDs\")\n\t}\n\n\treturn peers, nil\n}\n\nfunc (s *SqlStore) GetUserIDByPeerKey(ctx context.Context, lockStrength LockingStrength, peerKey string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar userID string\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tSelect(\"user_id\").\n\t\tTake(&userID, GetKeyQueryCondition(s), peerKey)\n\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn \"\", status.Errorf(status.NotFound, \"peer not found: index lookup failed\")\n\t\t}\n\t\treturn \"\", status.Errorf(status.Internal, \"failed to get user ID by peer key\")\n\t}\n\n\treturn userID, nil\n}\n\nfunc (s *SqlStore) CreateZone(ctx context.Context, zone *zones.Zone) error {\n\tresult := s.db.Create(zone)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create zone to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create zone to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) UpdateZone(ctx context.Context, zone *zones.Zone) error {\n\tresult := s.db.Select(\"*\").Save(zone)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update zone to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to update zone to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteZone(ctx context.Context, accountID, zoneID string) error {\n\tresult := s.db.Delete(&zones.Zone{}, accountAndIDQueryCondition, accountID, zoneID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete zone from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete zone from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewZoneNotFoundError(zoneID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetZoneByID(ctx context.Context, lockStrength LockingStrength, accountID, zoneID string) (*zones.Zone, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar zone *zones.Zone\n\tresult := tx.Preload(\"Records\").Take(&zone, accountAndIDQueryCondition, accountID, zoneID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewZoneNotFoundError(zoneID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get zone from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get zone from store\")\n\t}\n\n\treturn zone, nil\n}\n\nfunc (s *SqlStore) GetZoneByDomain(ctx context.Context, accountID, domain string) (*zones.Zone, error) {\n\tvar zone *zones.Zone\n\tresult := s.db.Where(\"account_id = ? AND domain = ?\", accountID, domain).First(&zone)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewZoneNotFoundError(domain)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get zone by domain from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get zone by domain from store\")\n\t}\n\n\treturn zone, nil\n}\n\nfunc (s *SqlStore) GetAccountZones(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*zones.Zone, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar zones []*zones.Zone\n\tresult := tx.Preload(\"Records\").Find(&zones, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get zones from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get zones from store\")\n\t}\n\n\treturn zones, nil\n}\n\nfunc (s *SqlStore) CreateDNSRecord(ctx context.Context, record *records.Record) error {\n\tresult := s.db.Create(record)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create dns record to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create dns record to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) UpdateDNSRecord(ctx context.Context, record *records.Record) error {\n\tresult := s.db.Select(\"*\").Save(record)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update dns record to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to update dns record to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteDNSRecord(ctx context.Context, accountID, zoneID, recordID string) error {\n\tresult := s.db.Delete(&records.Record{}, \"account_id = ? AND zone_id = ? AND id = ?\", accountID, zoneID, recordID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete dns record from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete dns record from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.NewDNSRecordNotFoundError(recordID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetDNSRecordByID(ctx context.Context, lockStrength LockingStrength, accountID, zoneID, recordID string) (*records.Record, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar record *records.Record\n\tresult := tx.Where(\"account_id = ? AND zone_id = ? AND id = ?\", accountID, zoneID, recordID).Take(&record)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.NewDNSRecordNotFoundError(recordID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get dns record from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get dns record from store\")\n\t}\n\n\treturn record, nil\n}\n\nfunc (s *SqlStore) GetZoneDNSRecords(ctx context.Context, lockStrength LockingStrength, accountID, zoneID string) ([]*records.Record, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar recordsList []*records.Record\n\tresult := tx.Where(\"account_id = ? AND zone_id = ?\", accountID, zoneID).Find(&recordsList)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get zone dns records from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get zone dns records from store\")\n\t}\n\n\treturn recordsList, nil\n}\n\nfunc (s *SqlStore) GetZoneDNSRecordsByName(ctx context.Context, lockStrength LockingStrength, accountID, zoneID, name string) ([]*records.Record, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar recordsList []*records.Record\n\tresult := tx.Where(\"account_id = ? AND zone_id = ? AND name = ?\", accountID, zoneID, name).Find(&recordsList)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get zone dns records by name from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get zone dns records by name from store\")\n\t}\n\n\treturn recordsList, nil\n}\n\nfunc (s *SqlStore) DeleteZoneDNSRecords(ctx context.Context, accountID, zoneID string) error {\n\tresult := s.db.Delete(&records.Record{}, \"account_id = ? AND zone_id = ?\", accountID, zoneID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete zone dns records from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete zone dns records from store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) GetPeerIDByKey(ctx context.Context, lockStrength LockingStrength, key string) (string, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar peerID string\n\tresult := tx.Model(&nbpeer.Peer{}).\n\t\tSelect(\"id\").\n\t\tWhere(GetKeyQueryCondition(s), key).\n\t\tLimit(1).\n\t\tScan(&peerID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get peer ID by key: %s\", result.Error)\n\t\treturn \"\", status.Errorf(status.Internal, \"failed to get peer ID by key\")\n\t}\n\n\treturn peerID, nil\n}\n\nfunc (s *SqlStore) CreateService(ctx context.Context, service *reverseproxy.Service) error {\n\tserviceCopy := service.Copy()\n\tif err := serviceCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn fmt.Errorf(\"encrypt service data: %w\", err)\n\t}\n\tresult := s.db.Create(serviceCopy)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create service to store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create service to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) UpdateService(ctx context.Context, service *reverseproxy.Service) error {\n\tserviceCopy := service.Copy()\n\tif err := serviceCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn fmt.Errorf(\"encrypt service data: %w\", err)\n\t}\n\n\t// Use a transaction to ensure atomic updates of the service and its targets\n\terr := s.db.Transaction(func(tx *gorm.DB) error {\n\t\t// Delete existing targets\n\t\tif err := tx.Where(\"service_id = ?\", serviceCopy.ID).Delete(&reverseproxy.Target{}).Error; err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Update the service and create new targets\n\t\tif err := tx.Session(&gorm.Session{FullSaveAssociations: true}).Save(serviceCopy).Error; err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update service to store: %v\", err)\n\t\treturn status.Errorf(status.Internal, \"failed to update service to store\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteService(ctx context.Context, accountID, serviceID string) error {\n\tresult := s.db.Delete(&reverseproxy.Service{}, accountAndIDQueryCondition, accountID, serviceID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete service from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete service from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"service %s not found\", serviceID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteTarget(ctx context.Context, accountID string, serviceID string, targetID uint) error {\n\tresult := s.db.Delete(&reverseproxy.Target{}, \"account_id = ? AND service_id = ? AND id = ?\", accountID, serviceID, targetID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete target from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete target from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"target not found for service %s\", serviceID)\n\t}\n\n\treturn nil\n}\n\nfunc (s *SqlStore) DeleteServiceTargets(ctx context.Context, accountID string, serviceID string) error {\n\tresult := s.db.Delete(&reverseproxy.Target{}, \"account_id = ? AND service_id = ?\", accountID, serviceID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete targets from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete targets from store\")\n\t}\n\n\treturn nil\n}\n\n// GetTargetsByServiceID retrieves all targets for a given service\nfunc (s *SqlStore) GetTargetsByServiceID(ctx context.Context, lockStrength LockingStrength, accountID string, serviceID string) ([]*reverseproxy.Target, error) {\n\tvar targets []*reverseproxy.Target\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\tresult := tx.Where(\"account_id = ? AND service_id = ?\", accountID, serviceID).Find(&targets)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get targets from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get targets from store\")\n\t}\n\n\treturn targets, nil\n}\n\nfunc (s *SqlStore) GetServiceByID(ctx context.Context, lockStrength LockingStrength, accountID, serviceID string) (*reverseproxy.Service, error) {\n\ttx := s.db.Preload(\"Targets\")\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar service *reverseproxy.Service\n\tresult := tx.Take(&service, accountAndIDQueryCondition, accountID, serviceID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"service %s not found\", serviceID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get service from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get service from store\")\n\t}\n\n\tif err := service.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt service data: %w\", err)\n\t}\n\n\treturn service, nil\n}\n\nfunc (s *SqlStore) GetServicesByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*reverseproxy.Service, error) {\n\ttx := s.db.Preload(\"Targets\")\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar serviceList []*reverseproxy.Service\n\tresult := tx.Find(&serviceList, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get services from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get services from store\")\n\t}\n\n\tfor _, service := range serviceList {\n\t\tif err := service.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt service data: %w\", err)\n\t\t}\n\t}\n\n\treturn serviceList, nil\n}\n\nfunc (s *SqlStore) GetServiceByDomain(ctx context.Context, accountID, domain string) (*reverseproxy.Service, error) {\n\tvar service *reverseproxy.Service\n\tresult := s.db.Preload(\"Targets\").Where(\"account_id = ? AND domain = ?\", accountID, domain).First(&service)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"service with domain %s not found\", domain)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get service by domain from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get service by domain from store\")\n\t}\n\n\tif err := service.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\treturn nil, fmt.Errorf(\"decrypt service data: %w\", err)\n\t}\n\n\treturn service, nil\n}\n\nfunc (s *SqlStore) GetServices(ctx context.Context, lockStrength LockingStrength) ([]*reverseproxy.Service, error) {\n\ttx := s.db.Preload(\"Targets\")\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar serviceList []*reverseproxy.Service\n\tresult := tx.Find(&serviceList)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get services from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get services from store\")\n\t}\n\n\tfor _, service := range serviceList {\n\t\tif err := service.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt service data: %w\", err)\n\t\t}\n\t}\n\n\treturn serviceList, nil\n}\n\nfunc (s *SqlStore) GetAccountServices(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*reverseproxy.Service, error) {\n\ttx := s.db.Preload(\"Targets\")\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar serviceList []*reverseproxy.Service\n\tresult := tx.Find(&serviceList, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get services from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get services from store\")\n\t}\n\n\tfor _, service := range serviceList {\n\t\tif err := service.DecryptSensitiveData(s.fieldEncrypt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decrypt service data: %w\", err)\n\t\t}\n\t}\n\n\treturn serviceList, nil\n}\n\nfunc (s *SqlStore) GetCustomDomain(ctx context.Context, accountID string, domainID string) (*domain.Domain, error) {\n\ttx := s.db\n\n\tcustomDomain := &domain.Domain{}\n\tresult := tx.Take(&customDomain, accountAndIDQueryCondition, accountID, domainID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"custom domain %s not found\", domainID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get custom domain from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get custom domain from store\")\n\t}\n\n\treturn customDomain, nil\n}\n\nfunc (s *SqlStore) ListFreeDomains(ctx context.Context, accountID string) ([]string, error) {\n\treturn nil, nil\n}\n\nfunc (s *SqlStore) ListCustomDomains(ctx context.Context, accountID string) ([]*domain.Domain, error) {\n\ttx := s.db\n\n\tvar domains []*domain.Domain\n\tresult := tx.Find(&domains, accountIDCondition, accountID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get reverse proxy custom domains from the store: %s\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get reverse proxy custom domains from store\")\n\t}\n\n\treturn domains, nil\n}\n\nfunc (s *SqlStore) CreateCustomDomain(ctx context.Context, accountID string, domainName string, targetCluster string, validated bool) (*domain.Domain, error) {\n\tnewDomain := &domain.Domain{\n\t\tID: xid.New().String(), // Generate our own ID because gorm doesn't always configure the database to handle this for us.\n\t\tDomain: domainName,\n\t\tAccountID: accountID,\n\t\tTargetCluster: targetCluster,\n\t\tType: domain.TypeCustom,\n\t\tValidated: validated,\n\t}\n\tresult := s.db.Create(newDomain)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to create reverse proxy custom domain to store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to create reverse proxy custom domain to store\")\n\t}\n\n\treturn newDomain, nil\n}\n\nfunc (s *SqlStore) UpdateCustomDomain(ctx context.Context, accountID string, d *domain.Domain) (*domain.Domain, error) {\n\td.AccountID = accountID\n\tresult := s.db.Select(\"*\").Save(d)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to update reverse proxy custom domain to store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to update reverse proxy custom domain to store\")\n\t}\n\n\treturn d, nil\n}\n\nfunc (s *SqlStore) DeleteCustomDomain(ctx context.Context, accountID string, domainID string) error {\n\tresult := s.db.Delete(domain.Domain{}, accountAndIDQueryCondition, accountID, domainID)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete reverse proxy custom domain from store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to delete reverse proxy custom domain from store\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn status.Errorf(status.NotFound, \"reverse proxy custom domain %s not found\", domainID)\n\t}\n\n\treturn nil\n}\n\n// CreateAccessLog creates a new access log entry in the database\nfunc (s *SqlStore) CreateAccessLog(ctx context.Context, logEntry *accesslogs.AccessLogEntry) error {\n\tresult := s.db.Create(logEntry)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).WithFields(log.Fields{\n\t\t\t\"service_id\": logEntry.ServiceID,\n\t\t\t\"method\": logEntry.Method,\n\t\t\t\"host\": logEntry.Host,\n\t\t\t\"path\": logEntry.Path,\n\t\t}).Errorf(\"failed to create access log entry in store: %v\", result.Error)\n\t\treturn status.Errorf(status.Internal, \"failed to create access log entry in store\")\n\t}\n\treturn nil\n}\n\n// GetAccountAccessLogs retrieves access logs for a given account with pagination and filtering\nfunc (s *SqlStore) GetAccountAccessLogs(ctx context.Context, lockStrength LockingStrength, accountID string, filter accesslogs.AccessLogFilter) ([]*accesslogs.AccessLogEntry, int64, error) {\n\tvar logs []*accesslogs.AccessLogEntry\n\tvar totalCount int64\n\n\tbaseQuery := s.db.WithContext(ctx).\n\t\tModel(&accesslogs.AccessLogEntry{}).\n\t\tWhere(accountIDCondition, accountID)\n\n\tbaseQuery = s.applyAccessLogFilters(baseQuery, filter)\n\n\tif err := baseQuery.Count(&totalCount).Error; err != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to count access logs: %v\", err)\n\t\treturn nil, 0, status.Errorf(status.Internal, \"failed to count access logs\")\n\t}\n\n\tquery := s.db.WithContext(ctx).\n\t\tWhere(accountIDCondition, accountID)\n\n\tquery = s.applyAccessLogFilters(query, filter)\n\n\tsortColumns := filter.GetSortColumn()\n\tsortOrder := strings.ToUpper(filter.GetSortOrder())\n\n\tvar orderClauses []string\n\tfor _, col := range strings.Split(sortColumns, \",\") {\n\t\tcol = strings.TrimSpace(col)\n\t\tif col != \"\" {\n\t\t\torderClauses = append(orderClauses, col+\" \"+sortOrder)\n\t\t}\n\t}\n\torderClause := strings.Join(orderClauses, \", \")\n\n\tquery = query.\n\t\tOrder(orderClause).\n\t\tLimit(filter.GetLimit()).\n\t\tOffset(filter.GetOffset())\n\n\tif lockStrength != LockingStrengthNone {\n\t\tquery = query.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tresult := query.Find(&logs)\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to get access logs from store: %v\", result.Error)\n\t\treturn nil, 0, status.Errorf(status.Internal, \"failed to get access logs from store\")\n\t}\n\n\treturn logs, totalCount, nil\n}\n\n// DeleteOldAccessLogs deletes all access logs older than the specified time\nfunc (s *SqlStore) DeleteOldAccessLogs(ctx context.Context, olderThan time.Time) (int64, error) {\n\tresult := s.db.WithContext(ctx).\n\t\tWhere(\"timestamp < ?\", olderThan).\n\t\tDelete(&accesslogs.AccessLogEntry{})\n\n\tif result.Error != nil {\n\t\tlog.WithContext(ctx).Errorf(\"failed to delete old access logs: %v\", result.Error)\n\t\treturn 0, status.Errorf(status.Internal, \"failed to delete old access logs\")\n\t}\n\n\treturn result.RowsAffected, nil\n}\n\n// applyAccessLogFilters applies filter conditions to the query\nfunc (s *SqlStore) applyAccessLogFilters(query *gorm.DB, filter accesslogs.AccessLogFilter) *gorm.DB {\n\tif filter.Search != nil {\n\t\tsearchPattern := \"%\" + *filter.Search + \"%\"\n\t\tquery = query.Where(\n\t\t\t\"id LIKE ? OR location_connection_ip LIKE ? OR host LIKE ? OR path LIKE ? OR CONCAT(host, path) LIKE ? OR user_id IN (SELECT id FROM users WHERE email LIKE ? OR name LIKE ?)\",\n\t\t\tsearchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern,\n\t\t)\n\t}\n\n\tif filter.SourceIP != nil {\n\t\tquery = query.Where(\"location_connection_ip = ?\", *filter.SourceIP)\n\t}\n\n\tif filter.Host != nil {\n\t\tquery = query.Where(\"host = ?\", *filter.Host)\n\t}\n\n\tif filter.Path != nil {\n\t\t// Support LIKE pattern for path filtering\n\t\tquery = query.Where(\"path LIKE ?\", \"%\"+*filter.Path+\"%\")\n\t}\n\n\tif filter.UserID != nil {\n\t\tquery = query.Where(\"user_id = ?\", *filter.UserID)\n\t}\n\n\tif filter.Method != nil {\n\t\tquery = query.Where(\"method = ?\", *filter.Method)\n\t}\n\n\tif filter.Status != nil {\n\t\tswitch *filter.Status {\n\t\tcase \"success\":\n\t\t\tquery = query.Where(\"status_code >= ? AND status_code < ?\", 200, 400)\n\t\tcase \"failed\":\n\t\t\tquery = query.Where(\"status_code < ? OR status_code >= ?\", 200, 400)\n\t\t}\n\t}\n\n\tif filter.StatusCode != nil {\n\t\tquery = query.Where(\"status_code = ?\", *filter.StatusCode)\n\t}\n\n\tif filter.StartDate != nil {\n\t\tquery = query.Where(\"timestamp >= ?\", *filter.StartDate)\n\t}\n\n\tif filter.EndDate != nil {\n\t\tquery = query.Where(\"timestamp <= ?\", *filter.EndDate)\n\t}\n\n\treturn query\n}\n\nfunc (s *SqlStore) GetServiceTargetByTargetID(ctx context.Context, lockStrength LockingStrength, accountID string, targetID string) (*reverseproxy.Target, error) {\n\ttx := s.db\n\tif lockStrength != LockingStrengthNone {\n\t\ttx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})\n\t}\n\n\tvar target *reverseproxy.Target\n\tresult := tx.Take(&target, \"account_id = ? AND target_id = ?\", accountID, targetID)\n\tif result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, status.Errorf(status.NotFound, \"service target with ID %s not found\", targetID)\n\t\t}\n\n\t\tlog.WithContext(ctx).Errorf(\"failed to get service target from store: %v\", result.Error)\n\t\treturn nil, status.Errorf(status.Internal, \"failed to get service target from store\")\n\t}\n\n\treturn target, nil\n}\n","structuredPatch":[{"oldStart":2728,"oldLines":14,"newStart":2728,"newLines":28,"lines":[" "," // NewSqliteStore creates a new SQLite store."," func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {","- storeStr := fmt.Sprintf(\"%s?cache=shared\", storeSqliteFileName)","- if runtime.GOOS == \"windows\" {","- // Vo avoid `The process cannot access the file because it is being used by another process` on Windows","- storeStr = storeSqliteFileName","+ storeFile := storeSqliteFileName","+ if envFile, ok := os.LookupEnv(\"NB_STORE_ENGINE_SQLITE_FILE\"); ok && envFile != \"\" {","+ storeFile = envFile"," }"," ","- file := filepath.Join(dataDir, storeStr)","- db, err := gorm.Open(sqlite.Open(file), getGormConfig())","+ // Separate file path from any SQLite URI query parameters (e.g., \"store.db?mode=rwc\")","+ filePath, query, hasQuery := strings.Cut(storeFile, \"?\")","+","+ connStr := filePath","+ if !filepath.IsAbs(filePath) {","+ connStr = filepath.Join(dataDir, filePath)","+ }","+","+ // Append query parameters: user-provided take precedence, otherwise default to cache=shared on non-Windows","+ if hasQuery {","+ connStr += \"?\" + query","+ } else if runtime.GOOS != \"windows\" {","+ // To avoid `The process cannot access the file because it is being used by another process` on Windows","+ connStr += \"?cache=shared\"","+ }","+","+ db, err := gorm.Open(sqlite.Open(connStr), getGormConfig())"," if err != nil {"," return nil, err"," }"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"962b799a-edbd-4945-a9ff-13ee3d250238"} +{"parentUuid":"5b922543-3105-449a-b4d5-979335b5aefe","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01UyDuky4UBSQvWuEwke5Lm2","type":"message","role":"assistant","content":[{"type":"text","text":"Now verify `strings` is imported in that file:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":3026,"cache_read_input_tokens":75745,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":3026},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYentL7aXNJyCshaHV1u3","type":"assistant","uuid":"a7367fcf-bb64-476a-a237-cc9272c10752","timestamp":"2026-03-02T19:17:57.196Z"} +{"parentUuid":"a7367fcf-bb64-476a-a237-cc9272c10752","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01UyDuky4UBSQvWuEwke5Lm2","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01KbgoFsd6GcuLQaFw5CtsAW","name":"Grep","input":{"pattern":"\\\"strings\\\"","path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/store/sql_store.go","output_mode":"content"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":3026,"cache_read_input_tokens":75745,"output_tokens":131,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":3026,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYentL7aXNJyCshaHV1u3","type":"assistant","uuid":"e4fd5add-0e8d-4a3c-937d-b6467f30ba50","timestamp":"2026-03-02T19:17:58.069Z"} +{"parentUuid":"e4fd5add-0e8d-4a3c-937d-b6467f30ba50","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Grep","command":"callback"},"parentToolUseID":"toolu_01KbgoFsd6GcuLQaFw5CtsAW","toolUseID":"toolu_01KbgoFsd6GcuLQaFw5CtsAW","timestamp":"2026-03-02T19:17:58.625Z","uuid":"81a11519-cd08-4781-baa8-2c053db27ea9"} +{"parentUuid":"e4fd5add-0e8d-4a3c-937d-b6467f30ba50","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01KbgoFsd6GcuLQaFw5CtsAW","type":"tool_result","content":"15:\t\"strings\""}]},"uuid":"54a69707-83cd-4507-bece-1dfe60dbc4d9","timestamp":"2026-03-02T19:17:58.623Z","toolUseResult":{"mode":"content","numFiles":0,"filenames":[],"content":"15:\t\"strings\"","numLines":1},"sourceToolAssistantUUID":"e4fd5add-0e8d-4a3c-937d-b6467f30ba50"} +{"parentUuid":"54a69707-83cd-4507-bece-1dfe60dbc4d9","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01D1jH5rGZPyNVpjVAWvwToe","type":"message","role":"assistant","content":[{"type":"text","text":"Good, `strings` is already imported. Now update the activity store:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":149,"cache_read_input_tokens":78771,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":149},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYenthPzmVVp3MSDQByEM","type":"assistant","uuid":"c548b00a-1e04-4175-b100-dde1c82af1d0","timestamp":"2026-03-02T19:18:02.926Z"} +{"parentUuid":"c548b00a-1e04-4175-b100-dde1c82af1d0","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01D1jH5rGZPyNVpjVAWvwToe","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01CT1CTfrqKaGzN4mQqZwaqz","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go","old_string":"\tswitch storeEngine {\n\tcase types.SqliteStoreEngine:\n\t\tdialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB))","new_string":"\tswitch storeEngine {\n\tcase types.SqliteStoreEngine:\n\t\tdbFile := eventSinkDB\n\t\tif envFile, ok := os.LookupEnv(\"NB_ACTIVITY_EVENT_SQLITE_FILE\"); ok && envFile != \"\" {\n\t\t\tdbFile = envFile\n\t\t}\n\t\t// Separate file path from any SQLite URI query parameters\n\t\tfilePath, query, hasQuery := strings.Cut(dbFile, \"?\")\n\t\tconnStr := filePath\n\t\tif !filepath.IsAbs(filePath) {\n\t\t\tconnStr = filepath.Join(dataDir, filePath)\n\t\t}\n\t\tif hasQuery {\n\t\t\tconnStr += \"?\" + query\n\t\t}\n\t\tdialector = sqlite.Open(connStr)"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":149,"cache_read_input_tokens":78771,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":149},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYenthPzmVVp3MSDQByEM","type":"assistant","uuid":"386e7aec-fe92-454e-b615-98f0437f538d","timestamp":"2026-03-02T19:18:15.536Z"} +{"parentUuid":"386e7aec-fe92-454e-b615-98f0437f538d","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"type":"tool_result","content":"The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.","is_error":true,"tool_use_id":"toolu_01CT1CTfrqKaGzN4mQqZwaqz"}]},"uuid":"2e79d59a-6d1e-4a4d-abec-2a3c135cecff","timestamp":"2026-03-02T19:19:49.631Z","toolUseResult":"User rejected tool use","sourceToolAssistantUUID":"386e7aec-fe92-454e-b615-98f0437f538d"} +{"parentUuid":"2e79d59a-6d1e-4a4d-abec-2a3c135cecff","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"type":"text","text":"[Request interrupted by user for tool use]"}]},"uuid":"b8549252-739f-4847-9d91-dd7a47325ebd","timestamp":"2026-03-02T19:19:49.634Z"} +{"type":"file-history-snapshot","messageId":"7c71f5f8-6fbe-48a5-8d9b-9322a19b9593","snapshot":{"messageId":"7c71f5f8-6fbe-48a5-8d9b-9322a19b9593","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v2","version":2,"backupTime":"2026-03-02T19:15:31.656Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v2","version":2,"backupTime":"2026-03-02T19:20:06.400Z"}},"timestamp":"2026-03-02T19:20:06.399Z"},"isSnapshotUpdate":false} +{"parentUuid":"b8549252-739f-4847-9d91-dd7a47325ebd","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":"question, do we need to do the query separation for activity store?"},"uuid":"7c71f5f8-6fbe-48a5-8d9b-9322a19b9593","timestamp":"2026-03-02T19:20:05.360Z","permissionMode":"default"} +{"parentUuid":"7c71f5f8-6fbe-48a5-8d9b-9322a19b9593","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01Mqs8etUWnqog28TYTsdYZk","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user is asking whether we need to handle query parameter separation for the activity store's SQLite file path.\n\nLet me look at the activity store code again. The current code is:\n```go\ndialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB))\n```\n\nUnlike the main store, the activity store does NOT append `?cache=shared` by default. So there's no default query parameter logic to worry about.\n\nThe question is: does the user need to be able to provide query params for the activity store's SQLite file?\n\nIf we're adding a `file` config field, and the user could theoretically pass something like `/data/events.db?cache=shared`, then yes we'd need to handle it for `filepath.IsAbs` to work correctly. But realistically:\n\n\nSince the activity store doesn't use `?cache=shared` by default and users will almost always provide plain file paths, we probably don't need query parameter handling here. Even if someone does pass `/data/events.db?cache=shared`, `filepath.IsAbs` will still work correctly because it just checks if the path starts with `/`, and the query string gets passed through to SQLite as-is. ` → false, so we'd join it with the data directory ✓\n\nSo the logic actually works correctly — `filepath.IsAbs` properly distinguishes between absolute paths (which bypass joining) and relative paths (which get joined), and the query parameters don't interfere with that check since they come after the path component.\n\nFor the activity store implementation, I just need to check the environment variable and apply the `filepath.IsAbs` logic without worrying about stripping query params beforehand. filepath.IsAbs` because `filepath.IsAbs` just checks if the path starts with `/` (on Unix) or a drive letter (on Windows). The `?` doesn't affect this check.\n\nSo the answer is: No, we don't need query parameter separation for the activity store. We just need to check `filepath.IsAbs` to decide whether to join with dataDir or use directly. The query params, if present, will just be passed through transparently to `sqlite.Open()`.\n\nWait, but what about `filepath.Join` with a `?` in the filename? Let me think... `filepath.Join` calls `filepath.Clean` on the result. Does `filepath.Clean` do anything weird with `?`? No, `?` is not special in file paths. `filepath.Clean` only handles `.`, `..`, multiple slashes, etc. So `filepath.Join(\"/var/lib/netbird\", \"events.db?cache=shared\")` should give `/var/lib/netbird/events.db?cache=shared` which is correct.\n\nI'm setting up the database file logic by checking the environment variable first, then using `filepath.IsAbs` to determine whether to join it with the data directory or use it as-is, and passing the result directly to `sqlite.Open()`. Both `filepath.IsAbs` and `filepath.Join` handle query parameters transparently since `?` isn't special in file paths.","signature":"EuEmCkYICxgCKkCRulH2TR5erbPGbJ7MuG6mzarDzeba7inOL9MFNh9syQxJCwNn6IqV0IOAHr2lu7h0l0dF3CQ2fszZB5RCndPEEgzby2Cb+OI7CNkh5y8aDLjuALLVnTMwUw++XCIwlPhTe1JJPT6xMYiJeww70nB6NSd8JHcYa841p1tnTftZuC0YFMtZbeYtVY+TOq+MKsglIgcgDxJ8TWGQU7fsL0mP52rDeWhvcRvwhY+NZZQyajuUHEn4CilvirePbsP6EpijH760hiY36s2P+PvP3tHvboApKEStyRfVSZKnxy+iJYnAhbBVF18XXf1fIXNiWtQ2EVVrFPhnVTOr633jvRjp+WT5n+0Xpw9RJcPt357rWYMBa3Zm2P2xC0mmnbbhhi88fTdCQDA+DLCg+fAvipiGMkh21++ssUCQX/QCbyK7yMRNrtFlHyIU90dPg4Q0dYIX4yQA4qRMfNNdFqBfWm/7uGLIpK4fsqypXMoUhJfuoQgBIieEbTCAK6n0+LkTUrDGurIkxKHP1s6QzmBdgmjNkd0DgV4Pg7sXx6jViD6E5/YaaQbJapAmntZVzJWP8XFcpFeYNOgufl5qLqBVE9fz6zW4RfbFh49sNcZMjGws1EyCConR6X6V/4kcF/+pbXRPJkaGAzQTv26k2tENnN9qh65gDgzFaq1kpkQmOv7t1adkxmoRXBcuBfl7kEaqqRDPA1Fa6l5ugxk5KnDP2UrlPU9P4xmqrnxz+PTOxwwDgaeRw4OnN/jbrHvHpLZqMySRdvK4Y8hwS1hGGwu3uHhJj/WWm7xrdV//Qmls9LK4wf8fLEff8fKr++4GLRCiK0FOsGvYo1VB/sjtThtHsDyKd2IhXZEmLwdOt9H1CFFq7M2r97bngVSoFlgiCWkZPezG0CanN2fabfOViYs5b7s12NB8fHSqfMGtoY3TxvGaZ5S4NeFFPV1rpJROlahA6xK/2gL/Uyka51gx/Ow2cvKSxcXwpYAlUmk+wIASQanzW4uq45Q7u0rUJ7sKdEWeo5lv3eb2dGbd+PRKp+4RKkT4+z3D9R54eI3K3kdTQFXY+/ALW2qB1kJzGO/+DmI0zFAAMJJGGMrrQAiZQGjj8LFD1FDkIBOvDkp0ZVwr7cJfSgiG/u4/gautjSWb0H48SpK1SVZcsDH4HcmsPqgMXyRUOV9Hb5RbgBJUylsKV5zqv4gJr/HuGK9ORDUb383WgHJb98Z8p3qmSDyX1VLN1zrmihgjGlGefGQ1lyQfXOGjhfRA51ujA6dxyt3vGbRgiaK3K5ZJu3EPhRI0abEFRTGJbyAOEN6joZpkyfPMFQqP2kU4S8lPelCOvWWeLEpCHlXxYl9i8U5dDGQgEBP4Agia37WCXkbx1cwe41hMQzr5PJqEyeldrVOKjRpQX17zBP5XJe4gnhxjxUto0ekhbvI8BKokoKZOo/gdZe0x+V8E5mlK8FnoAMXafizKdCJQuAc9jkJx6KNkI5obPIN52IHgFsngt7jfdz5JvR98OWcvfLVGPBqyZPjFgq0lBYmpgcf9R+LwOumKnm+mUzEZbWErL/QI6EkxYH0QXSu6wn9L4R5THWIoANSF/pQ2eGaQxA9cTRRpxQFsay0fXpRPNuzCS6kBQ0cZXqWsLeiLmYsW/cb1Z5Z1Hfpr+fKqqYDzl8DXBAYBIPgxe0VlkbzEhmrvV2OWUFaSz1UWEAZS/cYZfFxVNuURvhEd6rAL49OeI0iwVS3eYB8WykUxt0uTqjRDIfEuW5eLvbLhTcacpNa3HlLSx47nEYl1LPYexZwc5pZwdEP/Y6DJz8TUYV0oa2xCGODH7SuYvWnlYygA9daD5f3LZbCrXOAQ/1NbUy14GZqclNoAkTzmqjvcfqwrXzyjBE7+/3pEUBvNgbHQX8452p0uofazMckFwV01ZLNcc4sXuieGE5iFsrF0c0S1DBkRg3qjvCQsX2/hTtaG6gbv9wsr4g+PDIY17uuRRkA36zb2TiO1PzmzP85I9i4ugZljpB06pjbbvYcKhEGI9ooUluKJPV4FyWG3NlUO2V1jJKNURMGHrb3KEjqibtovC9hOVa2fDMtwCkps0lm/9bXBdBJM5C/WILLZ4PrqiiJtfnLC98ugM28E79+8+viBLNlYhXLPGUK7xLyk2EKzm4pWFLV4d1rRAuNi7pedozfGlgLZhwEKzJka8WQIWp51Zy96c/yxjEnU0HRA0S265N/k/hB15z9d4lQDB/YyJn5F2aGdg24zcQ+jOYgn4iWaHIeOQ6eP0jxXmvVHhhmix0vB2Oh5n93AEUaxdoUAtF9EDhOIgQfgp+WjiWYhGG09/p331S0u/p08SYXRVLOWUn60r1xQ8iNIGCxBGpFaFiRoxqkSQMUZIjcyiLRxv6sJB9/W2kdr2HZDgL7Hb6Oyd8HKwslLSxvb/pez0YbIkDy3sC0cu4b8L9CNolyKy9jcStbT+oSHp+/ELTsnPiYvn/4JtpcZhC4dQ/oXFiV3rNAZOzaYY/MAW3j/sMECKrJVPTMMRc2HmRTL2+YlOlT3E3nBBoe8/a+KPX1Y4iLmIfvuWntwWuaYAvhW7igTjHX+MrKF6hweKvTuGFD+63v0wpI3y+12pPy/vbaHcJR4lWJ4Kqg1PsQkmw5fvl9ESSIvl4ARSt9DaTOGM7YeG5KBnrDo2jR5rUB5Sf5slC6j1JTw8Eb91iMqWohMNjQBVat0ucGnxMNGnvQHSFbRv892CqXeNsTljtI8Yi7E0sXXDjduuhn3/RyzNtApNNyBA+argL6suHIpnxMyCF4azpdbyV1/QIWZwwBRpcqGfedUjnWvbMMuy3eLHZyaHCTSQ62mWjJfJBVoW+FsaVVP9QsKG/fNgjPxXXGKs4diU0P42a9wB0MXVb9QOeze5oAOhKYysFPgmxejRrRo/LzCjumEm5eVR5ghMsr06FeTBiCB3Hv3Ubi26g+6CNB6BJf3pqhhzjoTU2+8XlJbXI7yoDPpJmUHtENhzNVc721h5d9cmrh7rfKCoO5vZke/AaQnQ9KL7AjBcSf2oO3B7+peuMtaZMkJ2pEI4lok/FZVXlix0ocLrWJ1JbFuqA5B0gcsn6iQLvnQPOpxV8SVwSCoG0x8dJqX7oSNtKcxowmOI6ElbeCM4BRb4pSjtr6633N/zYi68noAqY7yLBAA6+VGZyI+jOSnhm86cnL+h7XhHqRFOHzxiGKQazqTByGdY4eSuECwY65sAHbxiv9C4gJK+foCvnCFc7i8fEWGO0/hIOG6mYyjUOIEcIi+oN+egix6iCFzWRwrIajX4X7kcc0xLDWzraFG5IHKFFvXQechdsOEor8e7p5vBmErzVLzpkTwJZGhcEICPHpCShRkPuGCrifzfBAR15khbcbbMHxB4UEsefou5GlntHBbYjyszhiIG9D8cjKMiLnYLI2xTWqd+LnP+bCe0mGpCp2zW8AFzBmGIrOakVJyY3UqcDQCnzc8/EaXshllctfkCWfLhWn/YxdM2mxh7QrwDnoB+FjB4a2WZdBRA6rrh++c/n3vOUaVTGXV+nrRuBQTXLaxLaPZ7k9UwY1d6AjCCLnsLFOlGPdgWQHiO26M5bpD2LdGrKoG3ooVwVVUKPX08FnpBPDb5cM9T4ErwdSQOeoiyJIpEWK5AtVCnGok0a4g/qlXM4o32OQRiaNZbM+TUL36ZDVF1+RzP+SUlIsjtnEyYaapzxQDuXxkg4EThuU/QfQ3+nMxXCV39uJV+H08/ZF90ELE+yX1bHUOH5Xg3QqqsCYICw2QAbCpuNwnYEYY8i+ql4c12vS1tV6YVk0PbI5tTjFUBnMxSeAAWjGg55TuMT/oWWXYHPnm1xpE8ZYwT2K7dUOTZErSE32iMnaCObgiDn6B2B56pkCozUf7oIiq4N8qUrFVTdUiDwboLCMVdwc6vyoTsTJb8pfi0XUWVMZo5qFBByNKw74S6FFn3qpslpw4fnwDyemls+kyPSKVqZYkGMSn+xyDWKKIiTZpBTOStQn63mwavKS3kH2sxpFqJTReQ+rXaT7GdXUkfCNbNC4YuZyfSjiUK1pLbOzJvFYXUlWHVaPUMFgV2FTYM9/CoEEb1up3q2Zy/61xOobgcUq/6rfFi04s+a1FEOB03oTPcJZC4zxoRf2qbh6xAUtEfcc7vK10vs382Ssr6leDJYrs5JPWCscXF2Ss0WDUL+PqLTPQMTCab/VUN6UlnSl2e38uw9NpoSSunZw6CjKaI6YF9ngDOuxf9MFd9qjR4yzzJJWKG9mM3CxeWKjm8eIe3A8V+ihXQbdG+uwYv+OXlUu5/00cnxAGl7fqgInAN3eGgiInWTkXC9aUNp3gVHVh1ZO/vrpp8V88jxhAanl0+cm4HEMN2BmGs5eWkHHt6XE21CpdMwnwEJifOAjeT5WK6PG7LWYH1zC3Lt2O2sMroHC/XenU6i6E9Mx0aier0UYC3wN73135fNDe+BLBMLpMV+0xBvMEL0ruxmOZxwJcP6+dbCzfmlIMwINz6nbNjXeTvyVbXpDExZH186VIHjGuih1M1o5/KkNIP5WrruRnXtOvxL+Bq4ZYDn8p3sp4pExcx/RbcFHBPFCeGAdRmHuVfWSI28fO2MODrpwYG8bwHZyDFBlHyNcwMGQmlUeC053gWsvI03RmxgJKpZ+rkVgSzN+O9GAwVGePLb9uOkgevfceQm71WgKLpO3hDY9JNsdlOiWVc77SB3zjxk7Ob/4njeMSYE5SaHs9FBXdTqKS0n2YDPQTfwoG2qRTb53EEuDaZSVRugpZ+ktdU1AjLMWV1yYpCj6aMlqZYBNTKkopOoBRqS6Pt6NYp0/Zwa9ogejnK0OsgE0Q5cPLw0SnOLYJ6ipsLBN6GROWt+wQe1Nq99Q/YPinNodsX8amhi9aK160WGwEddUNhjkUff+kz9QqRFSmYbk6i6A5Xzp9vGcN+Sqr4AZKqTnX9dd6IxKmP1aSmE0RSWTz5jJ4ZrYf+G6yTbMXbtr+017ZSBDI44OaJrSh5XBnvtH+0nYjbbfu+3IY/KK2umxeW4+KV2vc2Gx0wag6hizXkWV6TghTlWPgGsQonpDlwRUnTXCe4Z6PIATMHPhmVlbs048LzPR/Tx6Y/vKzLoz286v3r7tVzAgI+E+73yOHfSpxKb9j77rM9feGT8Si07A7Couw0G1QuPSGvxrXBys+LY/P8ya5clhbnBcmF8QLo0wzUZKvS6oWqPLlm0tx6710lR9cJ3x/fHp8NVbEsltYZKvITpBt9VyJ++UBOUjcf9CCUf1yHvTnA/rdsTtDJtzU1PvLQvsd1jj7EeluXd+3s+cS5pq3VtSMTDWkt2TZpu76M0rRkiHZXkvHuON7TsDUOXT1JBdd+GfU3PO/FpfnPM9opGp3Hb4z4scDjKO9jP5FMt2sjHJ4Dd0BKd0y2U79bRHWpLcJt4282eSFuE2HLx/oJiPzhXGk+hCtONa3YP/oFxRHpdNs0dXhkDMInfF2ouY4AL5Z0LWv5kbXtRpNGVFYRxsOr7c2p9fsKI2HL6apR3sYnyOmRKa78JK+QNYfWzZNbIKvQ62SopqsYfLYyZDUmjsMx/j1d13keCvOH7xrjReUBc2fy+vOLpf1Pq9s8lIsGocjq/iWUxOvTAKD4e6T8ZHqQmmOQWkWfGk7cINYrVr0+uGELjuE9HOTpvYTHM1IgwQN4WipspBzbxr/jtosWcB/cWe5+3tFMwuqRYM6L41b43U1bjaW2mRacnjVFu3szmS9n8+aaqmqjHFCyhBmJQCRH2ZAnX5sjkRqp8yTgP1SOaqPTr0Fyk0ria4zjB4K7LXYBGESLHJAPr1q63VC2XpdDF8WMbxWuMEV1ENn7or8M3+efE6XUo8RPLhflGQ6OukAn/XXckhgQ8v3y4kGV2sjZ/qlkyKjSrhcTk0UH4PYfdCQfVqyO39pwDZVZYs9zV3cFPa/vo/SmriC9vMWVJLfw3+dASDH5UXW6CJeU0T69muZnkBqzy3THl+RiEQlDaN+ATU4Ytzj9x1d2cvbWJrK4VSwzSMOca321qnAxqWIdlcPlgUNHDdq0JW4DvmOceYbMVkgJOXTIeUrgNXwdNdIunm2wMhdJH8pGsnkaNegXseCS5HrYXPav8anrt0eTKnHWt3StBCHE3BlSw4DPtdYW+XUd24lTN65y3j5lSDtGjeteM7Gm7TQGm4XLurVFUJOJe1Kgb6hgzRWNPZVzHUI4XlDGVX8EFPjxYJUT7VxFiDPWrUzlryfgJFebXC+Z0r3n0RNLN5POZYBKbZE/F88sHY+OiEpmks3iIOWjYf6hQ6k60gVLqD3ld+v/zrpIu//d3wmeqK7yUO+ZS9aPFApragQUNy/6I+pPQK4aE+ZQLsVx6cu6Ptr/YikVeeff5IxnrtF6qWH4q4Tiqn8L+62tpEhtwbx5yDsQCxGltwcfx5op5tjXNYXnlt5VloEpGvfaNuAy+kMc5WDUYValiJE4JUTYek5/zhbPaWX0Uy3FoXjdY7tsxKhzii085MRlseJPDvBc/fzPgC295Dg52iztqMTZIoMSYZlpsDJAzefHhTS+3ig1pxhsSs8Mbj8KnlvaW7aTaBkSp31/XCKdlNgjC/GG7iUzfgYAQ=="}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":732,"cache_read_input_tokens":78920,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":732},"output_tokens":10,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYeo46AiXZFD7S4sG8dLe","type":"assistant","uuid":"844d9322-9a89-4503-8db4-0d4c20dc85b0","timestamp":"2026-03-02T19:20:33.717Z"} +{"parentUuid":"844d9322-9a89-4503-8db4-0d4c20dc85b0","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01Mqs8etUWnqog28TYTsdYZk","type":"message","role":"assistant","content":[{"type":"text","text":"No, we don't. The query separation in the main store is only needed because it has conditional `?cache=shared` appending logic — we need to know whether the user already provided their own query params to avoid double-appending.\n\nThe activity store doesn't append any default query params. It just passes the path straight to `sqlite.Open()`. So we only need the `filepath.IsAbs` check to decide whether to join with `dataDir` or use it directly. `filepath.IsAbs` and `filepath.Join` both handle `?` fine since it's not a special character in file paths — it just passes through transparently to SQLite."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":732,"cache_read_input_tokens":78920,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":732},"output_tokens":10,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYeo46AiXZFD7S4sG8dLe","type":"assistant","uuid":"c86c982a-7db4-4fc5-b5ed-1a461d35be73","timestamp":"2026-03-02T19:20:36.337Z"} +{"parentUuid":"c86c982a-7db4-4fc5-b5ed-1a461d35be73","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01Mqs8etUWnqog28TYTsdYZk","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01RHfkzqoYbiKvQSd2WYfptF","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go","old_string":"\tswitch storeEngine {\n\tcase types.SqliteStoreEngine:\n\t\tdialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB))","new_string":"\tswitch storeEngine {\n\tcase types.SqliteStoreEngine:\n\t\tdbFile := eventSinkDB\n\t\tif envFile, ok := os.LookupEnv(\"NB_ACTIVITY_EVENT_SQLITE_FILE\"); ok && envFile != \"\" {\n\t\t\tdbFile = envFile\n\t\t}\n\t\tconnStr := dbFile\n\t\tif !filepath.IsAbs(dbFile) {\n\t\t\tconnStr = filepath.Join(dataDir, dbFile)\n\t\t}\n\t\tdialector = sqlite.Open(connStr)"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":732,"cache_read_input_tokens":78920,"output_tokens":1869,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":732,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYeo46AiXZFD7S4sG8dLe","type":"assistant","uuid":"ce67ac1c-c257-469a-945e-b239d82f41d3","timestamp":"2026-03-02T19:20:38.040Z"} +{"type":"file-history-snapshot","messageId":"ce67ac1c-c257-469a-945e-b239d82f41d3","snapshot":{"messageId":"7c71f5f8-6fbe-48a5-8d9b-9322a19b9593","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v2","version":2,"backupTime":"2026-03-02T19:15:31.656Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v2","version":2,"backupTime":"2026-03-02T19:20:06.400Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v1","version":1,"backupTime":"2026-03-02T19:21:36.151Z"}},"timestamp":"2026-03-02T19:20:06.399Z"},"isSnapshotUpdate":true} +{"parentUuid":"ce67ac1c-c257-469a-945e-b239d82f41d3","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_01RHfkzqoYbiKvQSd2WYfptF","toolUseID":"toolu_01RHfkzqoYbiKvQSd2WYfptF","timestamp":"2026-03-02T19:21:36.159Z","uuid":"1ff3a267-8127-495c-bd45-acd4ea2418a4"} +{"parentUuid":"ce67ac1c-c257-469a-945e-b239d82f41d3","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01RHfkzqoYbiKvQSd2WYfptF","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go has been updated successfully."}]},"uuid":"598070fa-d0ac-4c43-8dae-4a7ef248c712","timestamp":"2026-03-02T19:21:36.157Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go","oldString":"package store\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/driver/sqlite\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n\t\"gorm.io/gorm/logger\"\n\n\t\"github.com/netbirdio/netbird/management/server/activity\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n)\n\nconst (\n\t// eventSinkDB is the default name of the events database\n\teventSinkDB = \"events.db\"\n\n\tfallbackName = \"unknown\"\n\tfallbackEmail = \"unknown@unknown.com\"\n\n\tgcmEncAlgo = \"GCM\"\n\n\tstoreEngineEnv = \"NB_ACTIVITY_EVENT_STORE_ENGINE\"\n\tpostgresDsnEnv = \"NB_ACTIVITY_EVENT_POSTGRES_DSN\"\n\tsqlMaxOpenConnsEnv = \"NB_SQL_MAX_OPEN_CONNS\"\n)\n\ntype eventWithNames struct {\n\tactivity.Event\n\tInitiatorName string\n\tInitiatorEmail string\n\tTargetName string\n\tTargetEmail string\n}\n\n// Store is the implementation of the activity.Store interface backed by SQLite\ntype Store struct {\n\tdb *gorm.DB\n\tfieldEncrypt *crypt.FieldEncrypt\n}\n\n// NewSqlStore creates a new Store with an event table if not exists.\nfunc NewSqlStore(ctx context.Context, dataDir string, encryptionKey string) (*Store, error) {\n\tfieldEncrypt, err := crypt.NewFieldEncrypt(encryptionKey)\n\tif err != nil {\n\n\t\treturn nil, err\n\t}\n\n\tdb, err := initDatabase(ctx, dataDir)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"initialize database: %w\", err)\n\t}\n\n\tif err = migrate(ctx, fieldEncrypt, db); err != nil {\n\t\treturn nil, fmt.Errorf(\"events database migration: %w\", err)\n\t}\n\n\terr = db.AutoMigrate(&activity.Event{}, &activity.DeletedUser{})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"events auto migrate: %w\", err)\n\t}\n\n\treturn &Store{\n\t\tdb: db,\n\t\tfieldEncrypt: fieldEncrypt,\n\t}, nil\n}\n\nfunc (store *Store) processResult(ctx context.Context, events []*eventWithNames) ([]*activity.Event, error) {\n\tactivityEvents := make([]*activity.Event, 0)\n\tvar cryptErr error\n\n\tfor _, event := range events {\n\t\te := event.Event\n\t\tif e.Meta == nil {\n\t\t\te.Meta = make(map[string]any)\n\t\t}\n\n\t\tif event.TargetName != \"\" {\n\t\t\tname, err := store.fieldEncrypt.Decrypt(event.TargetName)\n\t\t\tif err != nil {\n\t\t\t\tcryptErr = fmt.Errorf(\"failed to decrypt username for target id: %s\", event.TargetName)\n\t\t\t\te.Meta[\"username\"] = fallbackName\n\t\t\t} else {\n\t\t\t\te.Meta[\"username\"] = name\n\t\t\t}\n\t\t}\n\n\t\tif event.TargetEmail != \"\" {\n\t\t\temail, err := store.fieldEncrypt.Decrypt(event.TargetEmail)\n\t\t\tif err != nil {\n\t\t\t\tcryptErr = fmt.Errorf(\"failed to decrypt email address for target id: %s\", event.TargetEmail)\n\t\t\t\te.Meta[\"email\"] = fallbackEmail\n\t\t\t} else {\n\t\t\t\te.Meta[\"email\"] = email\n\t\t\t}\n\t\t}\n\n\t\tif event.InitiatorName != \"\" {\n\t\t\tname, err := store.fieldEncrypt.Decrypt(event.InitiatorName)\n\t\t\tif err != nil {\n\t\t\t\tcryptErr = fmt.Errorf(\"failed to decrypt username of initiator: %s\", event.InitiatorName)\n\t\t\t\te.InitiatorName = fallbackName\n\t\t\t} else {\n\t\t\t\te.InitiatorName = name\n\t\t\t}\n\t\t}\n\n\t\tif event.InitiatorEmail != \"\" {\n\t\t\temail, err := store.fieldEncrypt.Decrypt(event.InitiatorEmail)\n\t\t\tif err != nil {\n\t\t\t\tcryptErr = fmt.Errorf(\"failed to decrypt email address of initiator: %s\", event.InitiatorEmail)\n\t\t\t\te.InitiatorEmail = fallbackEmail\n\t\t\t} else {\n\t\t\t\te.InitiatorEmail = email\n\t\t\t}\n\t\t}\n\n\t\tactivityEvents = append(activityEvents, &e)\n\t}\n\n\tif cryptErr != nil {\n\t\tlog.WithContext(ctx).Warnf(\"%s\", cryptErr)\n\t}\n\n\treturn activityEvents, nil\n}\n\n// Get returns \"limit\" number of events from index ordered descending or ascending by a timestamp\nfunc (store *Store) Get(ctx context.Context, accountID string, offset, limit int, descending bool) ([]*activity.Event, error) {\n\tbaseQuery := store.db.Model(&activity.Event{}).\n\t\tSelect(`\n events.*,\n u.name AS initiator_name,\n u.email AS initiator_email,\n t.name AS target_name,\n t.email AS target_email\n `).\n\t\tJoins(`LEFT JOIN deleted_users u ON u.id = events.initiator_id`).\n\t\tJoins(`LEFT JOIN deleted_users t ON t.id = events.target_id`)\n\n\torderDir := \"DESC\"\n\tif !descending {\n\t\torderDir = \"ASC\"\n\t}\n\n\tvar events []*eventWithNames\n\terr := baseQuery.Order(\"events.timestamp \"+orderDir).Offset(offset).Limit(limit).\n\t\tFind(&events, \"account_id = ?\", accountID).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn store.processResult(ctx, events)\n}\n\n// Save an event in the SQLite events table end encrypt the \"email\" element in meta map\nfunc (store *Store) Save(_ context.Context, event *activity.Event) (*activity.Event, error) {\n\teventCopy := event.Copy()\n\tmeta, err := store.saveDeletedUserEmailAndNameInEncrypted(eventCopy)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\teventCopy.Meta = meta\n\n\tif err = store.db.Create(eventCopy).Error; err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn eventCopy, nil\n}\n\n// saveDeletedUserEmailAndNameInEncrypted if the meta contains email and name then store it in encrypted way and delete\n// this item from meta map\nfunc (store *Store) saveDeletedUserEmailAndNameInEncrypted(event *activity.Event) (map[string]any, error) {\n\temail, ok := event.Meta[\"email\"]\n\tif !ok {\n\t\treturn event.Meta, nil\n\t}\n\n\tname, ok := event.Meta[\"name\"]\n\tif !ok {\n\t\treturn event.Meta, nil\n\t}\n\n\tdeletedUser := activity.DeletedUser{\n\t\tID: event.TargetID,\n\t\tEncAlgo: gcmEncAlgo,\n\t}\n\n\tencryptedEmail, err := store.fieldEncrypt.Encrypt(fmt.Sprintf(\"%s\", email))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdeletedUser.Email = encryptedEmail\n\n\tencryptedName, err := store.fieldEncrypt.Encrypt(fmt.Sprintf(\"%s\", name))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdeletedUser.Name = encryptedName\n\n\terr = store.db.Clauses(clause.OnConflict{\n\t\tColumns: []clause.Column{{Name: \"id\"}},\n\t\tDoUpdates: clause.AssignmentColumns([]string{\"email\", \"name\"}),\n\t}).Create(deletedUser).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(event.Meta) == 2 {\n\t\treturn nil, nil // nolint\n\t}\n\tdelete(event.Meta, \"email\")\n\tdelete(event.Meta, \"name\")\n\treturn event.Meta, nil\n}\n\n// Close the Store\nfunc (store *Store) Close(_ context.Context) error {\n\tif store.db != nil {\n\t\tsql, err := store.db.DB()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn sql.Close()\n\t}\n\treturn nil\n}\n\nfunc initDatabase(ctx context.Context, dataDir string) (*gorm.DB, error) {\n\tvar dialector gorm.Dialector\n\tvar storeEngine = types.SqliteStoreEngine\n\n\tif engine, ok := os.LookupEnv(storeEngineEnv); ok {\n\t\tstoreEngine = types.Engine(engine)\n\t}\n\n\tswitch storeEngine {\n\tcase types.SqliteStoreEngine:\n\t\tdialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB))\n\tcase types.PostgresStoreEngine:\n\t\tdsn, ok := os.LookupEnv(postgresDsnEnv)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"%s environment variable not set\", postgresDsnEnv)\n\t\t}\n\t\tdialector = postgres.Open(dsn)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported store engine: %s\", storeEngine)\n\t}\n\tlog.WithContext(ctx).Infof(\"using %s as activity event store engine\", storeEngine)\n\n\tdb, err := gorm.Open(dialector, &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open db connection: %w\", err)\n\t}\n\n\treturn configureConnectionPool(db, storeEngine)\n}\n\nfunc configureConnectionPool(db *gorm.DB, storeEngine types.Engine) (*gorm.DB, error) {\n\tsqlDB, err := db.DB()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconns, err := strconv.Atoi(os.Getenv(sqlMaxOpenConnsEnv))\n\tif err != nil {\n\t\tconns = runtime.NumCPU()\n\t}\n\tif storeEngine == types.SqliteStoreEngine {\n\t\tconns = 1\n\t}\n\n\tsqlDB.SetMaxOpenConns(conns)\n\tsqlDB.SetMaxIdleConns(conns)\n\tsqlDB.SetConnMaxLifetime(time.Hour)\n\tsqlDB.SetConnMaxIdleTime(3 * time.Minute)\n\n\tlog.Infof(\"Set max open db connections to %d, max idle to %d, max lifetime to %v, max idle time to %v\",\n\t\tconns, conns, time.Hour, 3*time.Minute)\n\n\treturn db, nil\n}","newString":"package store\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/driver/sqlite\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n\t\"gorm.io/gorm/logger\"\n\n\t\"github.com/netbirdio/netbird/management/server/activity\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n)\n\nconst (\n\t// eventSinkDB is the default name of the events database\n\teventSinkDB = \"events.db\"\n\n\tfallbackName = \"unknown\"\n\tfallbackEmail = \"unknown@unknown.com\"\n\n\tgcmEncAlgo = \"GCM\"\n\n\tstoreEngineEnv = \"NB_ACTIVITY_EVENT_STORE_ENGINE\"\n\tpostgresDsnEnv = \"NB_ACTIVITY_EVENT_POSTGRES_DSN\"\n\tsqlMaxOpenConnsEnv = \"NB_SQL_MAX_OPEN_CONNS\"\n)\n\ntype eventWithNames struct {\n\tactivity.Event\n\tInitiatorName string\n\tInitiatorEmail string\n\tTargetName string\n\tTargetEmail string\n}\n\n// Store is the implementation of the activity.Store interface backed by SQLite\ntype Store struct {\n\tdb *gorm.DB\n\tfieldEncrypt *crypt.FieldEncrypt\n}\n\n// NewSqlStore creates a new Store with an event table if not exists.\nfunc NewSqlStore(ctx context.Context, dataDir string, encryptionKey string) (*Store, error) {\n\tfieldEncrypt, err := crypt.NewFieldEncrypt(encryptionKey)\n\tif err != nil {\n\n\t\treturn nil, err\n\t}\n\n\tdb, err := initDatabase(ctx, dataDir)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"initialize database: %w\", err)\n\t}\n\n\tif err = migrate(ctx, fieldEncrypt, db); err != nil {\n\t\treturn nil, fmt.Errorf(\"events database migration: %w\", err)\n\t}\n\n\terr = db.AutoMigrate(&activity.Event{}, &activity.DeletedUser{})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"events auto migrate: %w\", err)\n\t}\n\n\treturn &Store{\n\t\tdb: db,\n\t\tfieldEncrypt: fieldEncrypt,\n\t}, nil\n}\n\nfunc (store *Store) processResult(ctx context.Context, events []*eventWithNames) ([]*activity.Event, error) {\n\tactivityEvents := make([]*activity.Event, 0)\n\tvar cryptErr error\n\n\tfor _, event := range events {\n\t\te := event.Event\n\t\tif e.Meta == nil {\n\t\t\te.Meta = make(map[string]any)\n\t\t}\n\n\t\tif event.TargetName != \"\" {\n\t\t\tname, err := store.fieldEncrypt.Decrypt(event.TargetName)\n\t\t\tif err != nil {\n\t\t\t\tcryptErr = fmt.Errorf(\"failed to decrypt username for target id: %s\", event.TargetName)\n\t\t\t\te.Meta[\"username\"] = fallbackName\n\t\t\t} else {\n\t\t\t\te.Meta[\"username\"] = name\n\t\t\t}\n\t\t}\n\n\t\tif event.TargetEmail != \"\" {\n\t\t\temail, err := store.fieldEncrypt.Decrypt(event.TargetEmail)\n\t\t\tif err != nil {\n\t\t\t\tcryptErr = fmt.Errorf(\"failed to decrypt email address for target id: %s\", event.TargetEmail)\n\t\t\t\te.Meta[\"email\"] = fallbackEmail\n\t\t\t} else {\n\t\t\t\te.Meta[\"email\"] = email\n\t\t\t}\n\t\t}\n\n\t\tif event.InitiatorName != \"\" {\n\t\t\tname, err := store.fieldEncrypt.Decrypt(event.InitiatorName)\n\t\t\tif err != nil {\n\t\t\t\tcryptErr = fmt.Errorf(\"failed to decrypt username of initiator: %s\", event.InitiatorName)\n\t\t\t\te.InitiatorName = fallbackName\n\t\t\t} else {\n\t\t\t\te.InitiatorName = name\n\t\t\t}\n\t\t}\n\n\t\tif event.InitiatorEmail != \"\" {\n\t\t\temail, err := store.fieldEncrypt.Decrypt(event.InitiatorEmail)\n\t\t\tif err != nil {\n\t\t\t\tcryptErr = fmt.Errorf(\"failed to decrypt email address of initiator: %s\", event.InitiatorEmail)\n\t\t\t\te.InitiatorEmail = fallbackEmail\n\t\t\t} else {\n\t\t\t\te.InitiatorEmail = email\n\t\t\t}\n\t\t}\n\n\t\tactivityEvents = append(activityEvents, &e)\n\t}\n\n\tif cryptErr != nil {\n\t\tlog.WithContext(ctx).Warnf(\"%s\", cryptErr)\n\t}\n\n\treturn activityEvents, nil\n}\n\n// Get returns \"limit\" number of events from index ordered descending or ascending by a timestamp\nfunc (store *Store) Get(ctx context.Context, accountID string, offset, limit int, descending bool) ([]*activity.Event, error) {\n\tbaseQuery := store.db.Model(&activity.Event{}).\n\t\tSelect(`\n events.*,\n u.name AS initiator_name,\n u.email AS initiator_email,\n t.name AS target_name,\n t.email AS target_email\n `).\n\t\tJoins(`LEFT JOIN deleted_users u ON u.id = events.initiator_id`).\n\t\tJoins(`LEFT JOIN deleted_users t ON t.id = events.target_id`)\n\n\torderDir := \"DESC\"\n\tif !descending {\n\t\torderDir = \"ASC\"\n\t}\n\n\tvar events []*eventWithNames\n\terr := baseQuery.Order(\"events.timestamp \"+orderDir).Offset(offset).Limit(limit).\n\t\tFind(&events, \"account_id = ?\", accountID).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn store.processResult(ctx, events)\n}\n\n// Save an event in the SQLite events table end encrypt the \"email\" element in meta map\nfunc (store *Store) Save(_ context.Context, event *activity.Event) (*activity.Event, error) {\n\teventCopy := event.Copy()\n\tmeta, err := store.saveDeletedUserEmailAndNameInEncrypted(eventCopy)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\teventCopy.Meta = meta\n\n\tif err = store.db.Create(eventCopy).Error; err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn eventCopy, nil\n}\n\n// saveDeletedUserEmailAndNameInEncrypted if the meta contains email and name then store it in encrypted way and delete\n// this item from meta map\nfunc (store *Store) saveDeletedUserEmailAndNameInEncrypted(event *activity.Event) (map[string]any, error) {\n\temail, ok := event.Meta[\"email\"]\n\tif !ok {\n\t\treturn event.Meta, nil\n\t}\n\n\tname, ok := event.Meta[\"name\"]\n\tif !ok {\n\t\treturn event.Meta, nil\n\t}\n\n\tdeletedUser := activity.DeletedUser{\n\t\tID: event.TargetID,\n\t\tEncAlgo: gcmEncAlgo,\n\t}\n\n\tencryptedEmail, err := store.fieldEncrypt.Encrypt(fmt.Sprintf(\"%s\", email))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdeletedUser.Email = encryptedEmail\n\n\tencryptedName, err := store.fieldEncrypt.Encrypt(fmt.Sprintf(\"%s\", name))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdeletedUser.Name = encryptedName\n\n\terr = store.db.Clauses(clause.OnConflict{\n\t\tColumns: []clause.Column{{Name: \"id\"}},\n\t\tDoUpdates: clause.AssignmentColumns([]string{\"email\", \"name\"}),\n\t}).Create(deletedUser).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(event.Meta) == 2 {\n\t\treturn nil, nil // nolint\n\t}\n\tdelete(event.Meta, \"email\")\n\tdelete(event.Meta, \"name\")\n\treturn event.Meta, nil\n}\n\n// Close the Store\nfunc (store *Store) Close(_ context.Context) error {\n\tif store.db != nil {\n\t\tsql, err := store.db.DB()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn sql.Close()\n\t}\n\treturn nil\n}\n\nfunc initDatabase(ctx context.Context, dataDir string) (*gorm.DB, error) {\n\tvar dialector gorm.Dialector\n\tvar storeEngine = types.SqliteStoreEngine\n\n\tif engine, ok := os.LookupEnv(storeEngineEnv); ok {\n\t\tstoreEngine = types.Engine(engine)\n\t}\n\n\tswitch storeEngine {\n\tcase types.SqliteStoreEngine:\n\t\tdbFile := eventSinkDB\n\t\tif envFile, ok := os.LookupEnv(\"NB_ACTIVITY_EVENT_SQLITE_FILE\"); ok && envFile != \"\" {\n\t\t\tdbFile = envFile\n\t\t}\n\t\tconnStr := dbFile\n\t\tif !filepath.IsAbs(dbFile) {\n\t\t\tconnStr = filepath.Join(dataDir, dbFile)\n\t\t}\n\t\tdialector = sqlite.Open(connStr)\n\tcase types.PostgresStoreEngine:\n\t\tdsn, ok := os.LookupEnv(postgresDsnEnv)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"%s environment variable not set\", postgresDsnEnv)\n\t\t}\n\t\tdialector = postgres.Open(dsn)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported store engine: %s\", storeEngine)\n\t}\n\tlog.WithContext(ctx).Infof(\"using %s as activity event store engine\", storeEngine)\n\n\tdb, err := gorm.Open(dialector, &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open db connection: %w\", err)\n\t}\n\n\treturn configureConnectionPool(db, storeEngine)\n}\n\nfunc configureConnectionPool(db *gorm.DB, storeEngine types.Engine) (*gorm.DB, error) {\n\tsqlDB, err := db.DB()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconns, err := strconv.Atoi(os.Getenv(sqlMaxOpenConnsEnv))\n\tif err != nil {\n\t\tconns = runtime.NumCPU()\n\t}\n\tif storeEngine == types.SqliteStoreEngine {\n\t\tconns = 1\n\t}\n\n\tsqlDB.SetMaxOpenConns(conns)\n\tsqlDB.SetMaxIdleConns(conns)\n\tsqlDB.SetConnMaxLifetime(time.Hour)\n\tsqlDB.SetConnMaxIdleTime(3 * time.Minute)\n\n\tlog.Infof(\"Set max open db connections to %d, max idle to %d, max lifetime to %v, max idle time to %v\",\n\t\tconns, conns, time.Hour, 3*time.Minute)\n\n\treturn db, nil\n}","originalFile":"package store\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/driver/sqlite\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n\t\"gorm.io/gorm/logger\"\n\n\t\"github.com/netbirdio/netbird/management/server/activity\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n)\n\nconst (\n\t// eventSinkDB is the default name of the events database\n\teventSinkDB = \"events.db\"\n\n\tfallbackName = \"unknown\"\n\tfallbackEmail = \"unknown@unknown.com\"\n\n\tgcmEncAlgo = \"GCM\"\n\n\tstoreEngineEnv = \"NB_ACTIVITY_EVENT_STORE_ENGINE\"\n\tpostgresDsnEnv = \"NB_ACTIVITY_EVENT_POSTGRES_DSN\"\n\tsqlMaxOpenConnsEnv = \"NB_SQL_MAX_OPEN_CONNS\"\n)\n\ntype eventWithNames struct {\n\tactivity.Event\n\tInitiatorName string\n\tInitiatorEmail string\n\tTargetName string\n\tTargetEmail string\n}\n\n// Store is the implementation of the activity.Store interface backed by SQLite\ntype Store struct {\n\tdb *gorm.DB\n\tfieldEncrypt *crypt.FieldEncrypt\n}\n\n// NewSqlStore creates a new Store with an event table if not exists.\nfunc NewSqlStore(ctx context.Context, dataDir string, encryptionKey string) (*Store, error) {\n\tfieldEncrypt, err := crypt.NewFieldEncrypt(encryptionKey)\n\tif err != nil {\n\n\t\treturn nil, err\n\t}\n\n\tdb, err := initDatabase(ctx, dataDir)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"initialize database: %w\", err)\n\t}\n\n\tif err = migrate(ctx, fieldEncrypt, db); err != nil {\n\t\treturn nil, fmt.Errorf(\"events database migration: %w\", err)\n\t}\n\n\terr = db.AutoMigrate(&activity.Event{}, &activity.DeletedUser{})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"events auto migrate: %w\", err)\n\t}\n\n\treturn &Store{\n\t\tdb: db,\n\t\tfieldEncrypt: fieldEncrypt,\n\t}, nil\n}\n\nfunc (store *Store) processResult(ctx context.Context, events []*eventWithNames) ([]*activity.Event, error) {\n\tactivityEvents := make([]*activity.Event, 0)\n\tvar cryptErr error\n\n\tfor _, event := range events {\n\t\te := event.Event\n\t\tif e.Meta == nil {\n\t\t\te.Meta = make(map[string]any)\n\t\t}\n\n\t\tif event.TargetName != \"\" {\n\t\t\tname, err := store.fieldEncrypt.Decrypt(event.TargetName)\n\t\t\tif err != nil {\n\t\t\t\tcryptErr = fmt.Errorf(\"failed to decrypt username for target id: %s\", event.TargetName)\n\t\t\t\te.Meta[\"username\"] = fallbackName\n\t\t\t} else {\n\t\t\t\te.Meta[\"username\"] = name\n\t\t\t}\n\t\t}\n\n\t\tif event.TargetEmail != \"\" {\n\t\t\temail, err := store.fieldEncrypt.Decrypt(event.TargetEmail)\n\t\t\tif err != nil {\n\t\t\t\tcryptErr = fmt.Errorf(\"failed to decrypt email address for target id: %s\", event.TargetEmail)\n\t\t\t\te.Meta[\"email\"] = fallbackEmail\n\t\t\t} else {\n\t\t\t\te.Meta[\"email\"] = email\n\t\t\t}\n\t\t}\n\n\t\tif event.InitiatorName != \"\" {\n\t\t\tname, err := store.fieldEncrypt.Decrypt(event.InitiatorName)\n\t\t\tif err != nil {\n\t\t\t\tcryptErr = fmt.Errorf(\"failed to decrypt username of initiator: %s\", event.InitiatorName)\n\t\t\t\te.InitiatorName = fallbackName\n\t\t\t} else {\n\t\t\t\te.InitiatorName = name\n\t\t\t}\n\t\t}\n\n\t\tif event.InitiatorEmail != \"\" {\n\t\t\temail, err := store.fieldEncrypt.Decrypt(event.InitiatorEmail)\n\t\t\tif err != nil {\n\t\t\t\tcryptErr = fmt.Errorf(\"failed to decrypt email address of initiator: %s\", event.InitiatorEmail)\n\t\t\t\te.InitiatorEmail = fallbackEmail\n\t\t\t} else {\n\t\t\t\te.InitiatorEmail = email\n\t\t\t}\n\t\t}\n\n\t\tactivityEvents = append(activityEvents, &e)\n\t}\n\n\tif cryptErr != nil {\n\t\tlog.WithContext(ctx).Warnf(\"%s\", cryptErr)\n\t}\n\n\treturn activityEvents, nil\n}\n\n// Get returns \"limit\" number of events from index ordered descending or ascending by a timestamp\nfunc (store *Store) Get(ctx context.Context, accountID string, offset, limit int, descending bool) ([]*activity.Event, error) {\n\tbaseQuery := store.db.Model(&activity.Event{}).\n\t\tSelect(`\n events.*,\n u.name AS initiator_name,\n u.email AS initiator_email,\n t.name AS target_name,\n t.email AS target_email\n `).\n\t\tJoins(`LEFT JOIN deleted_users u ON u.id = events.initiator_id`).\n\t\tJoins(`LEFT JOIN deleted_users t ON t.id = events.target_id`)\n\n\torderDir := \"DESC\"\n\tif !descending {\n\t\torderDir = \"ASC\"\n\t}\n\n\tvar events []*eventWithNames\n\terr := baseQuery.Order(\"events.timestamp \"+orderDir).Offset(offset).Limit(limit).\n\t\tFind(&events, \"account_id = ?\", accountID).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn store.processResult(ctx, events)\n}\n\n// Save an event in the SQLite events table end encrypt the \"email\" element in meta map\nfunc (store *Store) Save(_ context.Context, event *activity.Event) (*activity.Event, error) {\n\teventCopy := event.Copy()\n\tmeta, err := store.saveDeletedUserEmailAndNameInEncrypted(eventCopy)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\teventCopy.Meta = meta\n\n\tif err = store.db.Create(eventCopy).Error; err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn eventCopy, nil\n}\n\n// saveDeletedUserEmailAndNameInEncrypted if the meta contains email and name then store it in encrypted way and delete\n// this item from meta map\nfunc (store *Store) saveDeletedUserEmailAndNameInEncrypted(event *activity.Event) (map[string]any, error) {\n\temail, ok := event.Meta[\"email\"]\n\tif !ok {\n\t\treturn event.Meta, nil\n\t}\n\n\tname, ok := event.Meta[\"name\"]\n\tif !ok {\n\t\treturn event.Meta, nil\n\t}\n\n\tdeletedUser := activity.DeletedUser{\n\t\tID: event.TargetID,\n\t\tEncAlgo: gcmEncAlgo,\n\t}\n\n\tencryptedEmail, err := store.fieldEncrypt.Encrypt(fmt.Sprintf(\"%s\", email))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdeletedUser.Email = encryptedEmail\n\n\tencryptedName, err := store.fieldEncrypt.Encrypt(fmt.Sprintf(\"%s\", name))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdeletedUser.Name = encryptedName\n\n\terr = store.db.Clauses(clause.OnConflict{\n\t\tColumns: []clause.Column{{Name: \"id\"}},\n\t\tDoUpdates: clause.AssignmentColumns([]string{\"email\", \"name\"}),\n\t}).Create(deletedUser).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(event.Meta) == 2 {\n\t\treturn nil, nil // nolint\n\t}\n\tdelete(event.Meta, \"email\")\n\tdelete(event.Meta, \"name\")\n\treturn event.Meta, nil\n}\n\n// Close the Store\nfunc (store *Store) Close(_ context.Context) error {\n\tif store.db != nil {\n\t\tsql, err := store.db.DB()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn sql.Close()\n\t}\n\treturn nil\n}\n\nfunc initDatabase(ctx context.Context, dataDir string) (*gorm.DB, error) {\n\tvar dialector gorm.Dialector\n\tvar storeEngine = types.SqliteStoreEngine\n\n\tif engine, ok := os.LookupEnv(storeEngineEnv); ok {\n\t\tstoreEngine = types.Engine(engine)\n\t}\n\n\tswitch storeEngine {\n\tcase types.SqliteStoreEngine:\n\t\tdialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB))\n\tcase types.PostgresStoreEngine:\n\t\tdsn, ok := os.LookupEnv(postgresDsnEnv)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"%s environment variable not set\", postgresDsnEnv)\n\t\t}\n\t\tdialector = postgres.Open(dsn)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported store engine: %s\", storeEngine)\n\t}\n\tlog.WithContext(ctx).Infof(\"using %s as activity event store engine\", storeEngine)\n\n\tdb, err := gorm.Open(dialector, &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open db connection: %w\", err)\n\t}\n\n\treturn configureConnectionPool(db, storeEngine)\n}\n\nfunc configureConnectionPool(db *gorm.DB, storeEngine types.Engine) (*gorm.DB, error) {\n\tsqlDB, err := db.DB()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconns, err := strconv.Atoi(os.Getenv(sqlMaxOpenConnsEnv))\n\tif err != nil {\n\t\tconns = runtime.NumCPU()\n\t}\n\tif storeEngine == types.SqliteStoreEngine {\n\t\tconns = 1\n\t}\n\n\tsqlDB.SetMaxOpenConns(conns)\n\tsqlDB.SetMaxIdleConns(conns)\n\tsqlDB.SetConnMaxLifetime(time.Hour)\n\tsqlDB.SetConnMaxIdleTime(3 * time.Minute)\n\n\tlog.Infof(\"Set max open db connections to %d, max idle to %d, max lifetime to %v, max idle time to %v\",\n\t\tconns, conns, time.Hour, 3*time.Minute)\n\n\treturn db, nil\n}\n","structuredPatch":[{"oldStart":249,"oldLines":7,"newStart":249,"newLines":15,"lines":[" "," switch storeEngine {"," case types.SqliteStoreEngine:","- dialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB))","+ dbFile := eventSinkDB","+ if envFile, ok := os.LookupEnv(\"NB_ACTIVITY_EVENT_SQLITE_FILE\"); ok && envFile != \"\" {","+ dbFile = envFile","+ }","+ connStr := dbFile","+ if !filepath.IsAbs(dbFile) {","+ connStr = filepath.Join(dataDir, dbFile)","+ }","+ dialector = sqlite.Open(connStr)"," case types.PostgresStoreEngine:"," dsn, ok := os.LookupEnv(postgresDsnEnv)"," if !ok {"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"ce67ac1c-c257-469a-945e-b239d82f41d3"} +{"parentUuid":"598070fa-d0ac-4c43-8dae-4a7ef248c712","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01W8WaHEBHbhK8UNBASbb4c8","type":"message","role":"assistant","content":[{"type":"text","text":"Now verify `strings` import isn't needed (we didn't use it), but check if `os` is imported:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":1899,"cache_read_input_tokens":79652,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":1899},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYeoAjJYAj2UqpoXJn8rj","type":"assistant","uuid":"6d0b392a-ce2f-4658-a4b0-fd35db005c7a","timestamp":"2026-03-02T19:21:41.874Z"} +{"parentUuid":"6d0b392a-ce2f-4658-a4b0-fd35db005c7a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01W8WaHEBHbhK8UNBASbb4c8","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_018H69rWPZ9KSw6FBX92ouKx","name":"Grep","input":{"pattern":"\\\"os\\\"","path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/activity/store/sql_store.go","output_mode":"content"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":1899,"cache_read_input_tokens":79652,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":1899},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYeoAjJYAj2UqpoXJn8rj","type":"assistant","uuid":"88cd8009-d42b-4ce8-9160-93a157deec43","timestamp":"2026-03-02T19:21:42.922Z"} +{"parentUuid":"88cd8009-d42b-4ce8-9160-93a157deec43","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Grep","command":"callback"},"parentToolUseID":"toolu_018H69rWPZ9KSw6FBX92ouKx","toolUseID":"toolu_018H69rWPZ9KSw6FBX92ouKx","timestamp":"2026-03-02T19:21:43.627Z","uuid":"5a75c5ab-621e-4615-890b-f51d2a82eb62"} +{"parentUuid":"88cd8009-d42b-4ce8-9160-93a157deec43","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_018H69rWPZ9KSw6FBX92ouKx","type":"tool_result","content":"6:\t\"os\""}]},"uuid":"08c00ea7-18b6-4f77-b42b-3e4ef436d8a1","timestamp":"2026-03-02T19:21:43.626Z","toolUseResult":{"mode":"content","numFiles":0,"filenames":[],"content":"6:\t\"os\"","numLines":1},"sourceToolAssistantUUID":"88cd8009-d42b-4ce8-9160-93a157deec43"} +{"parentUuid":"08c00ea7-18b6-4f77-b42b-3e4ef436d8a1","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01JNJ14eMunZfdJENkCXos5b","type":"message","role":"assistant","content":[{"type":"text","text":"Good, `os` and `filepath` are already imported."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":165,"cache_read_input_tokens":81551,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":165},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYeoBH9DAv46dipMtxn12","type":"assistant","uuid":"fd65c064-6500-4b94-b062-6289c2f06871","timestamp":"2026-03-02T19:21:47.116Z"} +{"parentUuid":"fd65c064-6500-4b94-b062-6289c2f06871","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01JNJ14eMunZfdJENkCXos5b","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01WhKQd8gqm4df2NZPNZpgjK","name":"TaskUpdate","input":{"taskId":"2","status":"completed"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":165,"cache_read_input_tokens":81551,"output_tokens":84,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":165,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYeoBH9DAv46dipMtxn12","type":"assistant","uuid":"aaec79a2-f53a-49f1-b21f-da755b3500ad","timestamp":"2026-03-02T19:21:47.471Z"} +{"parentUuid":"aaec79a2-f53a-49f1-b21f-da755b3500ad","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01WhKQd8gqm4df2NZPNZpgjK","type":"tool_result","content":"Updated task #2 status"}]},"uuid":"38b04233-4f3b-4d84-b77d-c51a6e65d366","timestamp":"2026-03-02T19:21:47.517Z","toolUseResult":{"success":true,"taskId":"2","updatedFields":["status"],"statusChange":{"from":"in_progress","to":"completed"}},"sourceToolAssistantUUID":"aaec79a2-f53a-49f1-b21f-da755b3500ad"} +{"parentUuid":"38b04233-4f3b-4d84-b77d-c51a6e65d366","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01Dkq5vq7DP1xjVg535jmPL4","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01US9LEoVJgMFRSdLS6Adh1r","name":"TaskUpdate","input":{"taskId":"3","status":"in_progress"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":102,"cache_read_input_tokens":81716,"output_tokens":72,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":102,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYeoBZburWWmXTTYBYy5C","type":"assistant","uuid":"fabb9aa6-e267-4fa8-87f0-47ab19602c2c","timestamp":"2026-03-02T19:21:50.505Z"} +{"parentUuid":"fabb9aa6-e267-4fa8-87f0-47ab19602c2c","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01US9LEoVJgMFRSdLS6Adh1r","type":"tool_result","content":"Updated task #3 status"}]},"uuid":"422f8460-7ed0-485b-81a6-ae2bf48a210d","timestamp":"2026-03-02T19:21:50.552Z","toolUseResult":{"success":true,"taskId":"3","updatedFields":["status"],"statusChange":{"from":"pending","to":"in_progress"}},"sourceToolAssistantUUID":"fabb9aa6-e267-4fa8-87f0-47ab19602c2c"} +{"parentUuid":"422f8460-7ed0-485b-81a6-ae2bf48a210d","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01GiNV6xhupiwwuetiQBHWdZ","type":"message","role":"assistant","content":[{"type":"text","text":"Now set the env vars in `combined/cmd/root.go` `initializeConfig()` and `token.go`:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":90,"cache_read_input_tokens":81818,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":90},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYeoBnihkZB8KRSwQwGsf","type":"assistant","uuid":"cc9a3641-0a9f-4d52-96f3-be8397fa6c2c","timestamp":"2026-03-02T19:21:54.593Z"} +{"parentUuid":"cc9a3641-0a9f-4d52-96f3-be8397fa6c2c","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01GiNV6xhupiwwuetiQBHWdZ","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01GnxGefrDZaftiAEfx5giEd","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/root.go","offset":119,"limit":45},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":90,"cache_read_input_tokens":81818,"output_tokens":140,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":90,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYeoBnihkZB8KRSwQwGsf","type":"assistant","uuid":"cdcdc51b-31b9-4144-8b5f-f4da1c8e7746","timestamp":"2026-03-02T19:21:55.379Z"} +{"parentUuid":"cdcdc51b-31b9-4144-8b5f-f4da1c8e7746","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01GnxGefrDZaftiAEfx5giEd","toolUseID":"toolu_01GnxGefrDZaftiAEfx5giEd","timestamp":"2026-03-02T19:21:55.454Z","uuid":"0de066ae-7f64-4d2b-a3b1-8370a37d6e60"} +{"parentUuid":"cdcdc51b-31b9-4144-8b5f-f4da1c8e7746","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01GnxGefrDZaftiAEfx5giEd","type":"tool_result","content":" 119→// initializeConfig loads and validates the configuration, then initializes logging.\n 120→func initializeConfig() error {\n 121→\tvar err error\n 122→\tconfig, err = LoadConfig(configPath)\n 123→\tif err != nil {\n 124→\t\treturn fmt.Errorf(\"failed to load config: %w\", err)\n 125→\t}\n 126→\n 127→\tif err := config.Validate(); err != nil {\n 128→\t\treturn fmt.Errorf(\"invalid config: %w\", err)\n 129→\t}\n 130→\n 131→\tif err := util.InitLog(config.Server.LogLevel, config.Server.LogFile); err != nil {\n 132→\t\treturn fmt.Errorf(\"failed to initialize log: %w\", err)\n 133→\t}\n 134→\n 135→\tif dsn := config.Server.Store.DSN; dsn != \"\" {\n 136→\t\tswitch strings.ToLower(config.Server.Store.Engine) {\n 137→\t\tcase \"postgres\":\n 138→\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n 139→\t\tcase \"mysql\":\n 140→\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n 141→\t\t}\n 142→\t}\n 143→\n 144→\tif engine := config.Server.ActivityStore.Engine; engine != \"\" {\n 145→\t\tengineLower := strings.ToLower(engine)\n 146→\t\tif engineLower == \"postgres\" && config.Server.ActivityStore.DSN == \"\" {\n 147→\t\t\treturn fmt.Errorf(\"activityStore.dsn is required when activityStore.engine is postgres\")\n 148→\t\t}\n 149→\t\tos.Setenv(\"NB_ACTIVITY_EVENT_STORE_ENGINE\", engineLower)\n 150→\t\tif dsn := config.Server.ActivityStore.DSN; dsn != \"\" {\n 151→\t\t\tos.Setenv(\"NB_ACTIVITY_EVENT_POSTGRES_DSN\", dsn)\n 152→\t\t}\n 153→\t}\n 154→\n 155→\tlog.Infof(\"Starting combined NetBird server\")\n 156→\tlogConfig(config)\n 157→\tlogEnvVars()\n 158→\treturn nil\n 159→}\n 160→\n 161→// serverInstances holds all server instances created during startup.\n 162→type serverInstances struct {\n 163→\trelaySrv *relayServer.Server"}]},"uuid":"17337758-2aec-4472-82b8-75e656d551bb","timestamp":"2026-03-02T19:21:55.454Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/root.go","content":"// initializeConfig loads and validates the configuration, then initializes logging.\nfunc initializeConfig() error {\n\tvar err error\n\tconfig, err = LoadConfig(configPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to load config: %w\", err)\n\t}\n\n\tif err := config.Validate(); err != nil {\n\t\treturn fmt.Errorf(\"invalid config: %w\", err)\n\t}\n\n\tif err := util.InitLog(config.Server.LogLevel, config.Server.LogFile); err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize log: %w\", err)\n\t}\n\n\tif dsn := config.Server.Store.DSN; dsn != \"\" {\n\t\tswitch strings.ToLower(config.Server.Store.Engine) {\n\t\tcase \"postgres\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n\t\tcase \"mysql\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n\t\t}\n\t}\n\n\tif engine := config.Server.ActivityStore.Engine; engine != \"\" {\n\t\tengineLower := strings.ToLower(engine)\n\t\tif engineLower == \"postgres\" && config.Server.ActivityStore.DSN == \"\" {\n\t\t\treturn fmt.Errorf(\"activityStore.dsn is required when activityStore.engine is postgres\")\n\t\t}\n\t\tos.Setenv(\"NB_ACTIVITY_EVENT_STORE_ENGINE\", engineLower)\n\t\tif dsn := config.Server.ActivityStore.DSN; dsn != \"\" {\n\t\t\tos.Setenv(\"NB_ACTIVITY_EVENT_POSTGRES_DSN\", dsn)\n\t\t}\n\t}\n\n\tlog.Infof(\"Starting combined NetBird server\")\n\tlogConfig(config)\n\tlogEnvVars()\n\treturn nil\n}\n\n// serverInstances holds all server instances created during startup.\ntype serverInstances struct {\n\trelaySrv *relayServer.Server","numLines":45,"startLine":119,"totalLines":730}},"sourceToolAssistantUUID":"cdcdc51b-31b9-4144-8b5f-f4da1c8e7746"} +{"parentUuid":"17337758-2aec-4472-82b8-75e656d551bb","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_018hSnfbyoXUx2EoCRYrui9S","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_014zw4rer9cK6iwn8ZqNenVr","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/root.go","old_string":"\tif dsn := config.Server.Store.DSN; dsn != \"\" {\n\t\tswitch strings.ToLower(config.Server.Store.Engine) {\n\t\tcase \"postgres\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n\t\tcase \"mysql\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n\t\t}\n\t}\n\n\tif engine := config.Server.ActivityStore.Engine; engine != \"\" {\n\t\tengineLower := strings.ToLower(engine)\n\t\tif engineLower == \"postgres\" && config.Server.ActivityStore.DSN == \"\" {\n\t\t\treturn fmt.Errorf(\"activityStore.dsn is required when activityStore.engine is postgres\")\n\t\t}\n\t\tos.Setenv(\"NB_ACTIVITY_EVENT_STORE_ENGINE\", engineLower)\n\t\tif dsn := config.Server.ActivityStore.DSN; dsn != \"\" {\n\t\t\tos.Setenv(\"NB_ACTIVITY_EVENT_POSTGRES_DSN\", dsn)\n\t\t}\n\t}","new_string":"\tif dsn := config.Server.Store.DSN; dsn != \"\" {\n\t\tswitch strings.ToLower(config.Server.Store.Engine) {\n\t\tcase \"postgres\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n\t\tcase \"mysql\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n\t\t}\n\t}\n\tif file := config.Server.Store.File; file != \"\" {\n\t\tos.Setenv(\"NB_STORE_ENGINE_SQLITE_FILE\", file)\n\t}\n\n\tif engine := config.Server.ActivityStore.Engine; engine != \"\" {\n\t\tengineLower := strings.ToLower(engine)\n\t\tif engineLower == \"postgres\" && config.Server.ActivityStore.DSN == \"\" {\n\t\t\treturn fmt.Errorf(\"activityStore.dsn is required when activityStore.engine is postgres\")\n\t\t}\n\t\tos.Setenv(\"NB_ACTIVITY_EVENT_STORE_ENGINE\", engineLower)\n\t\tif dsn := config.Server.ActivityStore.DSN; dsn != \"\" {\n\t\t\tos.Setenv(\"NB_ACTIVITY_EVENT_POSTGRES_DSN\", dsn)\n\t\t}\n\t}\n\tif file := config.Server.ActivityStore.File; file != \"\" {\n\t\tos.Setenv(\"NB_ACTIVITY_EVENT_SQLITE_FILE\", file)\n\t}"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":783,"cache_read_input_tokens":81908,"output_tokens":750,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":783,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYeoC9VcXY2UW1Pz95B5r","type":"assistant","uuid":"aea0db76-4fc2-44fb-a18a-87e6326c1809","timestamp":"2026-03-02T19:22:10.187Z"} +{"type":"file-history-snapshot","messageId":"aea0db76-4fc2-44fb-a18a-87e6326c1809","snapshot":{"messageId":"7c71f5f8-6fbe-48a5-8d9b-9322a19b9593","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v2","version":2,"backupTime":"2026-03-02T19:15:31.656Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v2","version":2,"backupTime":"2026-03-02T19:20:06.400Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v1","version":1,"backupTime":"2026-03-02T19:21:36.151Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v1","version":1,"backupTime":"2026-03-02T19:22:27.975Z"}},"timestamp":"2026-03-02T19:20:06.399Z"},"isSnapshotUpdate":true} +{"parentUuid":"aea0db76-4fc2-44fb-a18a-87e6326c1809","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_014zw4rer9cK6iwn8ZqNenVr","toolUseID":"toolu_014zw4rer9cK6iwn8ZqNenVr","timestamp":"2026-03-02T19:22:27.980Z","uuid":"4545a003-a7c1-47cd-ab05-4c8a3f5bf51a"} +{"parentUuid":"aea0db76-4fc2-44fb-a18a-87e6326c1809","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_014zw4rer9cK6iwn8ZqNenVr","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/root.go has been updated successfully."}]},"uuid":"b625d80d-c176-45ce-8f4c-145ae323b4c8","timestamp":"2026-03-02T19:22:27.979Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/root.go","oldString":"package cmd\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/coder/websocket\"\n\t\"github.com/hashicorp/go-multierror\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"go.opentelemetry.io/otel/metric\"\n\t\"google.golang.org/grpc\"\n\n\t\"github.com/netbirdio/netbird/encryption\"\n\tmgmtServer \"github.com/netbirdio/netbird/management/internals/server\"\n\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n\t\"github.com/netbirdio/netbird/management/server/telemetry\"\n\t\"github.com/netbirdio/netbird/relay/healthcheck\"\n\trelayServer \"github.com/netbirdio/netbird/relay/server\"\n\t\"github.com/netbirdio/netbird/relay/server/listener/ws\"\n\tsharedMetrics \"github.com/netbirdio/netbird/shared/metrics\"\n\t\"github.com/netbirdio/netbird/shared/relay/auth\"\n\t\"github.com/netbirdio/netbird/shared/signal/proto\"\n\tsignalServer \"github.com/netbirdio/netbird/signal/server\"\n\t\"github.com/netbirdio/netbird/stun\"\n\t\"github.com/netbirdio/netbird/util\"\n\t\"github.com/netbirdio/netbird/util/wsproxy\"\n\twsproxyserver \"github.com/netbirdio/netbird/util/wsproxy/server\"\n)\n\nvar (\n\tconfigPath string\n\tconfig *CombinedConfig\n\n\trootCmd = &cobra.Command{\n\t\tUse: \"combined\",\n\t\tShort: \"Combined Netbird server (Management + Signal + Relay + STUN)\",\n\t\tLong: `Combined Netbird server for self-hosted deployments.\n\nAll services (Management, Signal, Relay) are multiplexed on a single port.\nOptional STUN server runs on separate UDP ports.\n\nConfiguration is loaded from a YAML file specified with --config.`,\n\t\tSilenceUsage: true,\n\t\tSilenceErrors: true,\n\t\tRunE: execute,\n\t}\n)\n\nfunc init() {\n\trootCmd.PersistentFlags().StringVarP(&configPath, \"config\", \"c\", \"\", \"path to YAML configuration file (required)\")\n\t_ = rootCmd.MarkPersistentFlagRequired(\"config\")\n\n\trootCmd.AddCommand(newTokenCommands())\n}\n\nfunc Execute() error {\n\treturn rootCmd.Execute()\n}\n\nfunc waitForExitSignal() {\n\tosSigs := make(chan os.Signal, 1)\n\tsignal.Notify(osSigs, syscall.SIGINT, syscall.SIGTERM)\n\t<-osSigs\n}\n\nfunc execute(cmd *cobra.Command, _ []string) error {\n\tif err := initializeConfig(); err != nil {\n\t\treturn err\n\t}\n\n\t// Management is required as the base server when signal or relay are enabled\n\tif (config.Signal.Enabled || config.Relay.Enabled) && !config.Management.Enabled {\n\t\treturn fmt.Errorf(\"management must be enabled when signal or relay are enabled (provides the base HTTP server)\")\n\t}\n\n\tservers, err := createAllServers(cmd.Context(), config)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Register services with management's gRPC server using AfterInit hook\n\tsetupServerHooks(servers, config)\n\n\t// Start management server (this also starts the HTTP listener)\n\tif servers.mgmtSrv != nil {\n\t\tif err := servers.mgmtSrv.Start(cmd.Context()); err != nil {\n\t\t\tcleanupSTUNListeners(servers.stunListeners)\n\t\t\treturn fmt.Errorf(\"failed to start management server: %w\", err)\n\t\t}\n\t}\n\n\t// Start all other servers\n\twg := sync.WaitGroup{}\n\tstartServers(&wg, servers.relaySrv, servers.healthcheck, servers.stunServer, servers.metricsServer)\n\n\twaitForExitSignal()\n\n\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\tdefer cancel()\n\n\terr = shutdownServers(ctx, servers.relaySrv, servers.healthcheck, servers.stunServer, servers.mgmtSrv, servers.metricsServer)\n\twg.Wait()\n\treturn err\n}\n\n// initializeConfig loads and validates the configuration, then initializes logging.\nfunc initializeConfig() error {\n\tvar err error\n\tconfig, err = LoadConfig(configPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to load config: %w\", err)\n\t}\n\n\tif err := config.Validate(); err != nil {\n\t\treturn fmt.Errorf(\"invalid config: %w\", err)\n\t}\n\n\tif err := util.InitLog(config.Server.LogLevel, config.Server.LogFile); err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize log: %w\", err)\n\t}\n\n\tif dsn := config.Server.Store.DSN; dsn != \"\" {\n\t\tswitch strings.ToLower(config.Server.Store.Engine) {\n\t\tcase \"postgres\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n\t\tcase \"mysql\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n\t\t}\n\t}\n\n\tif engine := config.Server.ActivityStore.Engine; engine != \"\" {\n\t\tengineLower := strings.ToLower(engine)\n\t\tif engineLower == \"postgres\" && config.Server.ActivityStore.DSN == \"\" {\n\t\t\treturn fmt.Errorf(\"activityStore.dsn is required when activityStore.engine is postgres\")\n\t\t}\n\t\tos.Setenv(\"NB_ACTIVITY_EVENT_STORE_ENGINE\", engineLower)\n\t\tif dsn := config.Server.ActivityStore.DSN; dsn != \"\" {\n\t\t\tos.Setenv(\"NB_ACTIVITY_EVENT_POSTGRES_DSN\", dsn)\n\t\t}\n\t}\n\n\tlog.Infof(\"Starting combined NetBird server\")\n\tlogConfig(config)\n\tlogEnvVars()\n\treturn nil\n}\n\n// serverInstances holds all server instances created during startup.\ntype serverInstances struct {\n\trelaySrv *relayServer.Server\n\tmgmtSrv *mgmtServer.BaseServer\n\tsignalSrv *signalServer.Server\n\thealthcheck *healthcheck.Server\n\tstunServer *stun.Server\n\tstunListeners []*net.UDPConn\n\tmetricsServer *sharedMetrics.Metrics\n}\n\n// createAllServers creates all server instances based on configuration.\nfunc createAllServers(ctx context.Context, cfg *CombinedConfig) (*serverInstances, error) {\n\tmetricsServer, err := sharedMetrics.NewServer(cfg.Server.MetricsPort, \"\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create metrics server: %w\", err)\n\t}\n\tservers := &serverInstances{\n\t\tmetricsServer: metricsServer,\n\t}\n\n\t_, tlsSupport, err := handleTLSConfig(cfg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to setup TLS config: %w\", err)\n\t}\n\n\tif err := servers.createRelayServer(cfg, tlsSupport); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := servers.createManagementServer(ctx, cfg); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := servers.createSignalServer(ctx, cfg); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := servers.createHealthcheckServer(cfg); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn servers, nil\n}\n\nfunc (s *serverInstances) createRelayServer(cfg *CombinedConfig, tlsSupport bool) error {\n\tif !cfg.Relay.Enabled {\n\t\treturn nil\n\t}\n\n\tvar err error\n\ts.stunListeners, err = createSTUNListeners(cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thashedSecret := sha256.Sum256([]byte(cfg.Relay.AuthSecret))\n\tauthenticator := auth.NewTimedHMACValidator(hashedSecret[:], 24*time.Hour)\n\n\trelayCfg := relayServer.Config{\n\t\tMeter: s.metricsServer.Meter,\n\t\tExposedAddress: cfg.Relay.ExposedAddress,\n\t\tAuthValidator: authenticator,\n\t\tTLSSupport: tlsSupport,\n\t}\n\n\ts.relaySrv, err = createRelayServer(relayCfg, s.stunListeners)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlog.Infof(\"Relay server created\")\n\n\tif len(s.stunListeners) > 0 {\n\t\ts.stunServer = stun.NewServer(s.stunListeners, cfg.Relay.Stun.LogLevel)\n\t}\n\n\treturn nil\n}\n\nfunc (s *serverInstances) createManagementServer(ctx context.Context, cfg *CombinedConfig) error {\n\tif !cfg.Management.Enabled {\n\t\treturn nil\n\t}\n\n\tmgmtConfig, err := cfg.ToManagementConfig()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create management config: %w\", err)\n\t}\n\n\t_, portStr, portErr := net.SplitHostPort(cfg.Server.ListenAddress)\n\tif portErr != nil {\n\t\tportStr = \"443\"\n\t}\n\tmgmtPort, _ := strconv.Atoi(portStr)\n\n\tif err := ApplyEmbeddedIdPConfig(ctx, mgmtConfig, mgmtPort, false); err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to apply embedded IdP config: %w\", err)\n\t}\n\n\tif err := EnsureEncryptionKey(ctx, mgmtConfig); err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to ensure encryption key: %w\", err)\n\t}\n\n\tLogConfigInfo(mgmtConfig)\n\n\ts.mgmtSrv, err = createManagementServer(cfg, mgmtConfig)\n\tif err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to create management server: %w\", err)\n\t}\n\n\t// Inject externally-managed AppMetrics so management uses the shared metrics server\n\tappMetrics, err := telemetry.NewAppMetricsWithMeter(ctx, s.metricsServer.Meter)\n\tif err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to create management app metrics: %w\", err)\n\t}\n\tmgmtServer.Inject[telemetry.AppMetrics](s.mgmtSrv, appMetrics)\n\n\tlog.Infof(\"Management server created\")\n\treturn nil\n}\n\nfunc (s *serverInstances) createSignalServer(ctx context.Context, cfg *CombinedConfig) error {\n\tif !cfg.Signal.Enabled {\n\t\treturn nil\n\t}\n\n\tvar err error\n\ts.signalSrv, err = signalServer.NewServer(ctx, s.metricsServer.Meter, \"signal_\")\n\tif err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to create signal server: %w\", err)\n\t}\n\n\tlog.Infof(\"Signal server created\")\n\treturn nil\n}\n\nfunc (s *serverInstances) createHealthcheckServer(cfg *CombinedConfig) error {\n\thCfg := healthcheck.Config{\n\t\tListenAddress: cfg.Server.HealthcheckAddress,\n\t\tServiceChecker: s.relaySrv,\n\t}\n\n\tvar err error\n\ts.healthcheck, err = createHealthCheck(hCfg, s.stunListeners)\n\treturn err\n}\n\n// setupServerHooks registers services with management's gRPC server.\nfunc setupServerHooks(servers *serverInstances, cfg *CombinedConfig) {\n\tif servers.mgmtSrv == nil {\n\t\treturn\n\t}\n\n\tservers.mgmtSrv.AfterInit(func(s *mgmtServer.BaseServer) {\n\t\tgrpcSrv := s.GRPCServer()\n\n\t\tif servers.signalSrv != nil {\n\t\t\tproto.RegisterSignalExchangeServer(grpcSrv, servers.signalSrv)\n\t\t\tlog.Infof(\"Signal server registered on port %s\", cfg.Server.ListenAddress)\n\t\t}\n\n\t\ts.SetHandlerFunc(createCombinedHandler(grpcSrv, s.APIHandler(), servers.relaySrv, servers.metricsServer.Meter, cfg))\n\t\tif servers.relaySrv != nil {\n\t\t\tlog.Infof(\"Relay WebSocket handler added (path: /relay)\")\n\t\t}\n\t})\n}\n\nfunc startServers(wg *sync.WaitGroup, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, metricsServer *sharedMetrics.Metrics) {\n\tif srv != nil {\n\t\tinstanceURL := srv.InstanceURL()\n\t\tlog.Infof(\"Relay server instance URL: %s\", instanceURL.String())\n\t\tlog.Infof(\"Relay WebSocket multiplexed on management port (no separate relay listener)\")\n\t}\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tlog.Infof(\"running metrics server: %s%s\", metricsServer.Addr, metricsServer.Endpoint)\n\t\tif err := metricsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {\n\t\t\tlog.Fatalf(\"failed to start metrics server: %v\", err)\n\t\t}\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tif err := httpHealthcheck.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {\n\t\t\tlog.Fatalf(\"failed to start healthcheck server: %v\", err)\n\t\t}\n\t}()\n\n\tif stunServer != nil {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tif err := stunServer.Listen(); err != nil {\n\t\t\t\tif errors.Is(err, stun.ErrServerClosed) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlog.Errorf(\"STUN server error: %v\", err)\n\t\t\t}\n\t\t}()\n\t}\n}\n\nfunc shutdownServers(ctx context.Context, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, mgmtSrv *mgmtServer.BaseServer, metricsServer *sharedMetrics.Metrics) error {\n\tvar errs error\n\n\tif err := httpHealthcheck.Shutdown(ctx); err != nil {\n\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close healthcheck server: %w\", err))\n\t}\n\n\tif stunServer != nil {\n\t\tif err := stunServer.Shutdown(); err != nil {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close STUN server: %w\", err))\n\t\t}\n\t}\n\n\tif srv != nil {\n\t\tif err := srv.Shutdown(ctx); err != nil {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close relay server: %w\", err))\n\t\t}\n\t}\n\n\tif mgmtSrv != nil {\n\t\tlog.Infof(\"shutting down management and signal servers\")\n\t\tif err := mgmtSrv.Stop(); err != nil {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close management server: %w\", err))\n\t\t}\n\t}\n\n\tif metricsServer != nil {\n\t\tlog.Infof(\"shutting down metrics server\")\n\t\tif err := metricsServer.Shutdown(ctx); err != nil {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close metrics server: %w\", err))\n\t\t}\n\t}\n\n\treturn errs\n}\n\nfunc createHealthCheck(hCfg healthcheck.Config, stunListeners []*net.UDPConn) (*healthcheck.Server, error) {\n\thttpHealthcheck, err := healthcheck.NewServer(hCfg)\n\tif err != nil {\n\t\tcleanupSTUNListeners(stunListeners)\n\t\treturn nil, fmt.Errorf(\"failed to create healthcheck server: %w\", err)\n\t}\n\treturn httpHealthcheck, nil\n}\n\nfunc createRelayServer(cfg relayServer.Config, stunListeners []*net.UDPConn) (*relayServer.Server, error) {\n\tsrv, err := relayServer.NewServer(cfg)\n\tif err != nil {\n\t\tcleanupSTUNListeners(stunListeners)\n\t\treturn nil, fmt.Errorf(\"failed to create relay server: %w\", err)\n\t}\n\treturn srv, nil\n}\n\nfunc cleanupSTUNListeners(stunListeners []*net.UDPConn) {\n\tfor _, l := range stunListeners {\n\t\t_ = l.Close()\n\t}\n}\n\nfunc createSTUNListeners(cfg *CombinedConfig) ([]*net.UDPConn, error) {\n\tvar stunListeners []*net.UDPConn\n\tif cfg.Relay.Stun.Enabled {\n\t\tfor _, port := range cfg.Relay.Stun.Ports {\n\t\t\tlistener, err := net.ListenUDP(\"udp\", &net.UDPAddr{Port: port})\n\t\t\tif err != nil {\n\t\t\t\tcleanupSTUNListeners(stunListeners)\n\t\t\t\treturn nil, fmt.Errorf(\"failed to create STUN listener on port %d: %w\", port, err)\n\t\t\t}\n\t\t\tstunListeners = append(stunListeners, listener)\n\t\t\tlog.Infof(\"STUN server listening on UDP port %d\", port)\n\t\t}\n\t}\n\treturn stunListeners, nil\n}\n\nfunc handleTLSConfig(cfg *CombinedConfig) (*tls.Config, bool, error) {\n\ttlsCfg := cfg.Server.TLS\n\n\tif tlsCfg.LetsEncrypt.AWSRoute53 {\n\t\tlog.Debugf(\"using Let's Encrypt DNS resolver with Route 53 support\")\n\t\tr53 := encryption.Route53TLS{\n\t\t\tDataDir: tlsCfg.LetsEncrypt.DataDir,\n\t\t\tEmail: tlsCfg.LetsEncrypt.Email,\n\t\t\tDomains: tlsCfg.LetsEncrypt.Domains,\n\t\t}\n\t\ttc, err := r53.GetCertificate()\n\t\tif err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\t\treturn tc, true, nil\n\t}\n\n\tif cfg.HasLetsEncrypt() {\n\t\tlog.Infof(\"setting up TLS with Let's Encrypt\")\n\t\tcertManager, err := encryption.CreateCertManager(tlsCfg.LetsEncrypt.DataDir, tlsCfg.LetsEncrypt.Domains...)\n\t\tif err != nil {\n\t\t\treturn nil, false, fmt.Errorf(\"failed creating LetsEncrypt cert manager: %w\", err)\n\t\t}\n\t\treturn certManager.TLSConfig(), true, nil\n\t}\n\n\tif cfg.HasTLSCert() {\n\t\tlog.Debugf(\"using file based TLS config\")\n\t\ttc, err := encryption.LoadTLSConfig(tlsCfg.CertFile, tlsCfg.KeyFile)\n\t\tif err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\t\treturn tc, true, nil\n\t}\n\n\treturn nil, false, nil\n}\n\nfunc createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (*mgmtServer.BaseServer, error) {\n\tmgmt := cfg.Management\n\n\tdnsDomain := mgmt.DnsDomain\n\tsingleAccModeDomain := dnsDomain\n\n\t// Extract port from listen address\n\t_, portStr, err := net.SplitHostPort(cfg.Server.ListenAddress)\n\tif err != nil {\n\t\t// If no port specified, assume default\n\t\tportStr = \"443\"\n\t}\n\tmgmtPort, _ := strconv.Atoi(portStr)\n\n\tmgmtSrv := mgmtServer.NewServer(\n\t\t&mgmtServer.Config{\n\t\t\tNbConfig: mgmtConfig,\n\t\t\tDNSDomain: dnsDomain,\n\t\t\tMgmtSingleAccModeDomain: singleAccModeDomain,\n\t\t\tMgmtPort: mgmtPort,\n\t\t\tMgmtMetricsPort: cfg.Server.MetricsPort,\n\t\t\tDisableMetrics: mgmt.DisableAnonymousMetrics,\n\t\t\tDisableGeoliteUpdate: mgmt.DisableGeoliteUpdate,\n\t\t\t// Always enable user deletion from IDP in combined server (embedded IdP is always enabled)\n\t\t\tUserDeleteFromIDPEnabled: true,\n\t\t},\n\t)\n\n\treturn mgmtSrv, nil\n}\n\n// createCombinedHandler creates an HTTP handler that multiplexes Management, Signal (via wsproxy), and Relay WebSocket traffic\nfunc createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, relaySrv *relayServer.Server, meter metric.Meter, cfg *CombinedConfig) http.Handler {\n\twsProxy := wsproxyserver.New(grpcServer, wsproxyserver.WithOTelMeter(meter))\n\n\tvar relayAcceptFn func(conn net.Conn)\n\tif relaySrv != nil {\n\t\trelayAcceptFn = relaySrv.RelayAccept()\n\t}\n\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch {\n\t\t// Native gRPC traffic (HTTP/2 with gRPC content-type)\n\t\tcase r.ProtoMajor == 2 && (strings.HasPrefix(r.Header.Get(\"Content-Type\"), \"application/grpc\") ||\n\t\t\tstrings.HasPrefix(r.Header.Get(\"Content-Type\"), \"application/grpc+proto\")):\n\t\t\tgrpcServer.ServeHTTP(w, r)\n\n\t\t// WebSocket proxy for Management gRPC\n\t\tcase r.URL.Path == wsproxy.ProxyPath+wsproxy.ManagementComponent:\n\t\t\twsProxy.Handler().ServeHTTP(w, r)\n\n\t\t// WebSocket proxy for Signal gRPC\n\t\tcase r.URL.Path == wsproxy.ProxyPath+wsproxy.SignalComponent:\n\t\t\tif cfg.Signal.Enabled {\n\t\t\t\twsProxy.Handler().ServeHTTP(w, r)\n\t\t\t} else {\n\t\t\t\thttp.Error(w, \"Signal service not enabled\", http.StatusNotFound)\n\t\t\t}\n\n\t\t// Relay WebSocket\n\t\tcase r.URL.Path == \"/relay\":\n\t\t\tif relayAcceptFn != nil {\n\t\t\t\thandleRelayWebSocket(w, r, relayAcceptFn, cfg)\n\t\t\t} else {\n\t\t\t\thttp.Error(w, \"Relay service not enabled\", http.StatusNotFound)\n\t\t\t}\n\n\t\t// Management HTTP API (default)\n\t\tdefault:\n\t\t\thttpHandler.ServeHTTP(w, r)\n\t\t}\n\t})\n}\n\n// handleRelayWebSocket handles incoming WebSocket connections for the relay service\nfunc handleRelayWebSocket(w http.ResponseWriter, r *http.Request, acceptFn func(conn net.Conn), cfg *CombinedConfig) {\n\tacceptOptions := &websocket.AcceptOptions{\n\t\tOriginPatterns: []string{\"*\"},\n\t}\n\n\twsConn, err := websocket.Accept(w, r, acceptOptions)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to accept relay ws connection: %s\", err)\n\t\treturn\n\t}\n\n\tconnRemoteAddr := r.RemoteAddr\n\tif r.Header.Get(\"X-Real-Ip\") != \"\" && r.Header.Get(\"X-Real-Port\") != \"\" {\n\t\tconnRemoteAddr = net.JoinHostPort(r.Header.Get(\"X-Real-Ip\"), r.Header.Get(\"X-Real-Port\"))\n\t}\n\n\trAddr, err := net.ResolveTCPAddr(\"tcp\", connRemoteAddr)\n\tif err != nil {\n\t\t_ = wsConn.Close(websocket.StatusInternalError, \"internal error\")\n\t\treturn\n\t}\n\n\tlAddr, err := net.ResolveTCPAddr(\"tcp\", cfg.Server.ListenAddress)\n\tif err != nil {\n\t\t_ = wsConn.Close(websocket.StatusInternalError, \"internal error\")\n\t\treturn\n\t}\n\n\tlog.Debugf(\"Relay WS client connected from: %s\", rAddr)\n\n\tconn := ws.NewConn(wsConn, lAddr, rAddr)\n\tacceptFn(conn)\n}\n\n// logConfig prints all configuration parameters for debugging\nfunc logConfig(cfg *CombinedConfig) {\n\tlog.Info(\"=== Configuration ===\")\n\tlogServerConfig(cfg)\n\tlogComponentsConfig(cfg)\n\tlogRelayConfig(cfg)\n\tlogManagementConfig(cfg)\n\tlog.Info(\"=== End Configuration ===\")\n}\n\nfunc logServerConfig(cfg *CombinedConfig) {\n\tlog.Info(\"--- Server ---\")\n\tlog.Infof(\" Listen address: %s\", cfg.Server.ListenAddress)\n\tlog.Infof(\" Exposed address: %s\", cfg.Server.ExposedAddress)\n\tlog.Infof(\" Healthcheck address: %s\", cfg.Server.HealthcheckAddress)\n\tlog.Infof(\" Metrics port: %d\", cfg.Server.MetricsPort)\n\tlog.Infof(\" Log level: %s\", cfg.Server.LogLevel)\n\tlog.Infof(\" Data dir: %s\", cfg.Server.DataDir)\n\n\tswitch {\n\tcase cfg.HasTLSCert():\n\t\tlog.Infof(\" TLS: cert=%s, key=%s\", cfg.Server.TLS.CertFile, cfg.Server.TLS.KeyFile)\n\tcase cfg.HasLetsEncrypt():\n\t\tlog.Infof(\" TLS: Let's Encrypt (domains=%v)\", cfg.Server.TLS.LetsEncrypt.Domains)\n\tdefault:\n\t\tlog.Info(\" TLS: disabled (using reverse proxy)\")\n\t}\n}\n\nfunc logComponentsConfig(cfg *CombinedConfig) {\n\tlog.Info(\"--- Components ---\")\n\tlog.Infof(\" Management: %v (log level: %s)\", cfg.Management.Enabled, cfg.Management.LogLevel)\n\tlog.Infof(\" Signal: %v (log level: %s)\", cfg.Signal.Enabled, cfg.Signal.LogLevel)\n\tlog.Infof(\" Relay: %v (log level: %s)\", cfg.Relay.Enabled, cfg.Relay.LogLevel)\n}\n\nfunc logRelayConfig(cfg *CombinedConfig) {\n\tif !cfg.Relay.Enabled {\n\t\treturn\n\t}\n\tlog.Info(\"--- Relay ---\")\n\tlog.Infof(\" Exposed address: %s\", cfg.Relay.ExposedAddress)\n\tlog.Infof(\" Auth secret: %s...\", maskSecret(cfg.Relay.AuthSecret))\n\tif cfg.Relay.Stun.Enabled {\n\t\tlog.Infof(\" STUN ports: %v (log level: %s)\", cfg.Relay.Stun.Ports, cfg.Relay.Stun.LogLevel)\n\t} else {\n\t\tlog.Info(\" STUN: disabled\")\n\t}\n}\n\nfunc logManagementConfig(cfg *CombinedConfig) {\n\tif !cfg.Management.Enabled {\n\t\treturn\n\t}\n\tlog.Info(\"--- Management ---\")\n\tlog.Infof(\" Data dir: %s\", cfg.Management.DataDir)\n\tlog.Infof(\" DNS domain: %s\", cfg.Management.DnsDomain)\n\tlog.Infof(\" Store engine: %s\", cfg.Management.Store.Engine)\n\tif cfg.Server.Store.DSN != \"\" {\n\t\tlog.Infof(\" Store DSN: %s\", maskDSNPassword(cfg.Server.Store.DSN))\n\t}\n\n\tlog.Info(\" Auth (embedded IdP):\")\n\tlog.Infof(\" Issuer: %s\", cfg.Management.Auth.Issuer)\n\tlog.Infof(\" Dashboard redirect URIs: %v\", cfg.Management.Auth.DashboardRedirectURIs)\n\tlog.Infof(\" CLI redirect URIs: %v\", cfg.Management.Auth.CLIRedirectURIs)\n\n\tlog.Info(\" Client settings:\")\n\tlog.Infof(\" Signal URI: %s\", cfg.Management.SignalURI)\n\tfor _, s := range cfg.Management.Stuns {\n\t\tlog.Infof(\" STUN: %s\", s.URI)\n\t}\n\tif len(cfg.Management.Relays.Addresses) > 0 {\n\t\tlog.Infof(\" Relay addresses: %v\", cfg.Management.Relays.Addresses)\n\t\tlog.Infof(\" Relay credentials TTL: %s\", cfg.Management.Relays.CredentialsTTL)\n\t}\n}\n\n// logEnvVars logs all NB_ environment variables that are currently set\nfunc logEnvVars() {\n\tlog.Info(\"=== Environment Variables ===\")\n\tfound := false\n\tfor _, env := range os.Environ() {\n\t\tif strings.HasPrefix(env, \"NB_\") {\n\t\t\tkey, _, _ := strings.Cut(env, \"=\")\n\t\t\tvalue := os.Getenv(key)\n\t\t\tkeyLower := strings.ToLower(key)\n\t\t\tif strings.Contains(keyLower, \"secret\") || strings.Contains(keyLower, \"key\") || strings.Contains(keyLower, \"password\") {\n\t\t\t\tvalue = maskSecret(value)\n\t\t\t} else if strings.Contains(keyLower, \"dsn\") {\n\t\t\t\tvalue = maskDSNPassword(value)\n\t\t\t}\n\t\t\tlog.Infof(\" %s=%s\", key, value)\n\t\t\tfound = true\n\t\t}\n\t}\n\tif !found {\n\t\tlog.Info(\" (none set)\")\n\t}\n\tlog.Info(\"=== End Environment Variables ===\")\n}\n\n// maskDSNPassword masks the password in a DSN string.\n// Handles both key=value format (\"password=secret\") and URI format (\"user:secret@host\").\nfunc maskDSNPassword(dsn string) string {\n\t// Key=value format: \"host=localhost user=nb password=secret dbname=nb\"\n\tif strings.Contains(dsn, \"password=\") {\n\t\tparts := strings.Fields(dsn)\n\t\tfor i, p := range parts {\n\t\t\tif strings.HasPrefix(p, \"password=\") {\n\t\t\t\tparts[i] = \"password=****\"\n\t\t\t}\n\t\t}\n\t\treturn strings.Join(parts, \" \")\n\t}\n\n\t// URI format: \"user:password@host...\"\n\tif atIdx := strings.Index(dsn, \"@\"); atIdx != -1 {\n\t\tprefix := dsn[:atIdx]\n\t\tif colonIdx := strings.Index(prefix, \":\"); colonIdx != -1 {\n\t\t\treturn prefix[:colonIdx+1] + \"****\" + dsn[atIdx:]\n\t\t}\n\t}\n\n\treturn dsn\n}\n\n// maskSecret returns first 4 chars of secret followed by \"...\"\nfunc maskSecret(secret string) string {\n\tif len(secret) <= 4 {\n\t\treturn \"****\"\n\t}\n\treturn secret[:4] + \"...\"\n}","newString":"package cmd\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/coder/websocket\"\n\t\"github.com/hashicorp/go-multierror\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"go.opentelemetry.io/otel/metric\"\n\t\"google.golang.org/grpc\"\n\n\t\"github.com/netbirdio/netbird/encryption\"\n\tmgmtServer \"github.com/netbirdio/netbird/management/internals/server\"\n\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n\t\"github.com/netbirdio/netbird/management/server/telemetry\"\n\t\"github.com/netbirdio/netbird/relay/healthcheck\"\n\trelayServer \"github.com/netbirdio/netbird/relay/server\"\n\t\"github.com/netbirdio/netbird/relay/server/listener/ws\"\n\tsharedMetrics \"github.com/netbirdio/netbird/shared/metrics\"\n\t\"github.com/netbirdio/netbird/shared/relay/auth\"\n\t\"github.com/netbirdio/netbird/shared/signal/proto\"\n\tsignalServer \"github.com/netbirdio/netbird/signal/server\"\n\t\"github.com/netbirdio/netbird/stun\"\n\t\"github.com/netbirdio/netbird/util\"\n\t\"github.com/netbirdio/netbird/util/wsproxy\"\n\twsproxyserver \"github.com/netbirdio/netbird/util/wsproxy/server\"\n)\n\nvar (\n\tconfigPath string\n\tconfig *CombinedConfig\n\n\trootCmd = &cobra.Command{\n\t\tUse: \"combined\",\n\t\tShort: \"Combined Netbird server (Management + Signal + Relay + STUN)\",\n\t\tLong: `Combined Netbird server for self-hosted deployments.\n\nAll services (Management, Signal, Relay) are multiplexed on a single port.\nOptional STUN server runs on separate UDP ports.\n\nConfiguration is loaded from a YAML file specified with --config.`,\n\t\tSilenceUsage: true,\n\t\tSilenceErrors: true,\n\t\tRunE: execute,\n\t}\n)\n\nfunc init() {\n\trootCmd.PersistentFlags().StringVarP(&configPath, \"config\", \"c\", \"\", \"path to YAML configuration file (required)\")\n\t_ = rootCmd.MarkPersistentFlagRequired(\"config\")\n\n\trootCmd.AddCommand(newTokenCommands())\n}\n\nfunc Execute() error {\n\treturn rootCmd.Execute()\n}\n\nfunc waitForExitSignal() {\n\tosSigs := make(chan os.Signal, 1)\n\tsignal.Notify(osSigs, syscall.SIGINT, syscall.SIGTERM)\n\t<-osSigs\n}\n\nfunc execute(cmd *cobra.Command, _ []string) error {\n\tif err := initializeConfig(); err != nil {\n\t\treturn err\n\t}\n\n\t// Management is required as the base server when signal or relay are enabled\n\tif (config.Signal.Enabled || config.Relay.Enabled) && !config.Management.Enabled {\n\t\treturn fmt.Errorf(\"management must be enabled when signal or relay are enabled (provides the base HTTP server)\")\n\t}\n\n\tservers, err := createAllServers(cmd.Context(), config)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Register services with management's gRPC server using AfterInit hook\n\tsetupServerHooks(servers, config)\n\n\t// Start management server (this also starts the HTTP listener)\n\tif servers.mgmtSrv != nil {\n\t\tif err := servers.mgmtSrv.Start(cmd.Context()); err != nil {\n\t\t\tcleanupSTUNListeners(servers.stunListeners)\n\t\t\treturn fmt.Errorf(\"failed to start management server: %w\", err)\n\t\t}\n\t}\n\n\t// Start all other servers\n\twg := sync.WaitGroup{}\n\tstartServers(&wg, servers.relaySrv, servers.healthcheck, servers.stunServer, servers.metricsServer)\n\n\twaitForExitSignal()\n\n\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\tdefer cancel()\n\n\terr = shutdownServers(ctx, servers.relaySrv, servers.healthcheck, servers.stunServer, servers.mgmtSrv, servers.metricsServer)\n\twg.Wait()\n\treturn err\n}\n\n// initializeConfig loads and validates the configuration, then initializes logging.\nfunc initializeConfig() error {\n\tvar err error\n\tconfig, err = LoadConfig(configPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to load config: %w\", err)\n\t}\n\n\tif err := config.Validate(); err != nil {\n\t\treturn fmt.Errorf(\"invalid config: %w\", err)\n\t}\n\n\tif err := util.InitLog(config.Server.LogLevel, config.Server.LogFile); err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize log: %w\", err)\n\t}\n\n\tif dsn := config.Server.Store.DSN; dsn != \"\" {\n\t\tswitch strings.ToLower(config.Server.Store.Engine) {\n\t\tcase \"postgres\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n\t\tcase \"mysql\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n\t\t}\n\t}\n\tif file := config.Server.Store.File; file != \"\" {\n\t\tos.Setenv(\"NB_STORE_ENGINE_SQLITE_FILE\", file)\n\t}\n\n\tif engine := config.Server.ActivityStore.Engine; engine != \"\" {\n\t\tengineLower := strings.ToLower(engine)\n\t\tif engineLower == \"postgres\" && config.Server.ActivityStore.DSN == \"\" {\n\t\t\treturn fmt.Errorf(\"activityStore.dsn is required when activityStore.engine is postgres\")\n\t\t}\n\t\tos.Setenv(\"NB_ACTIVITY_EVENT_STORE_ENGINE\", engineLower)\n\t\tif dsn := config.Server.ActivityStore.DSN; dsn != \"\" {\n\t\t\tos.Setenv(\"NB_ACTIVITY_EVENT_POSTGRES_DSN\", dsn)\n\t\t}\n\t}\n\tif file := config.Server.ActivityStore.File; file != \"\" {\n\t\tos.Setenv(\"NB_ACTIVITY_EVENT_SQLITE_FILE\", file)\n\t}\n\n\tlog.Infof(\"Starting combined NetBird server\")\n\tlogConfig(config)\n\tlogEnvVars()\n\treturn nil\n}\n\n// serverInstances holds all server instances created during startup.\ntype serverInstances struct {\n\trelaySrv *relayServer.Server\n\tmgmtSrv *mgmtServer.BaseServer\n\tsignalSrv *signalServer.Server\n\thealthcheck *healthcheck.Server\n\tstunServer *stun.Server\n\tstunListeners []*net.UDPConn\n\tmetricsServer *sharedMetrics.Metrics\n}\n\n// createAllServers creates all server instances based on configuration.\nfunc createAllServers(ctx context.Context, cfg *CombinedConfig) (*serverInstances, error) {\n\tmetricsServer, err := sharedMetrics.NewServer(cfg.Server.MetricsPort, \"\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create metrics server: %w\", err)\n\t}\n\tservers := &serverInstances{\n\t\tmetricsServer: metricsServer,\n\t}\n\n\t_, tlsSupport, err := handleTLSConfig(cfg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to setup TLS config: %w\", err)\n\t}\n\n\tif err := servers.createRelayServer(cfg, tlsSupport); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := servers.createManagementServer(ctx, cfg); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := servers.createSignalServer(ctx, cfg); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := servers.createHealthcheckServer(cfg); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn servers, nil\n}\n\nfunc (s *serverInstances) createRelayServer(cfg *CombinedConfig, tlsSupport bool) error {\n\tif !cfg.Relay.Enabled {\n\t\treturn nil\n\t}\n\n\tvar err error\n\ts.stunListeners, err = createSTUNListeners(cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thashedSecret := sha256.Sum256([]byte(cfg.Relay.AuthSecret))\n\tauthenticator := auth.NewTimedHMACValidator(hashedSecret[:], 24*time.Hour)\n\n\trelayCfg := relayServer.Config{\n\t\tMeter: s.metricsServer.Meter,\n\t\tExposedAddress: cfg.Relay.ExposedAddress,\n\t\tAuthValidator: authenticator,\n\t\tTLSSupport: tlsSupport,\n\t}\n\n\ts.relaySrv, err = createRelayServer(relayCfg, s.stunListeners)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlog.Infof(\"Relay server created\")\n\n\tif len(s.stunListeners) > 0 {\n\t\ts.stunServer = stun.NewServer(s.stunListeners, cfg.Relay.Stun.LogLevel)\n\t}\n\n\treturn nil\n}\n\nfunc (s *serverInstances) createManagementServer(ctx context.Context, cfg *CombinedConfig) error {\n\tif !cfg.Management.Enabled {\n\t\treturn nil\n\t}\n\n\tmgmtConfig, err := cfg.ToManagementConfig()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create management config: %w\", err)\n\t}\n\n\t_, portStr, portErr := net.SplitHostPort(cfg.Server.ListenAddress)\n\tif portErr != nil {\n\t\tportStr = \"443\"\n\t}\n\tmgmtPort, _ := strconv.Atoi(portStr)\n\n\tif err := ApplyEmbeddedIdPConfig(ctx, mgmtConfig, mgmtPort, false); err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to apply embedded IdP config: %w\", err)\n\t}\n\n\tif err := EnsureEncryptionKey(ctx, mgmtConfig); err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to ensure encryption key: %w\", err)\n\t}\n\n\tLogConfigInfo(mgmtConfig)\n\n\ts.mgmtSrv, err = createManagementServer(cfg, mgmtConfig)\n\tif err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to create management server: %w\", err)\n\t}\n\n\t// Inject externally-managed AppMetrics so management uses the shared metrics server\n\tappMetrics, err := telemetry.NewAppMetricsWithMeter(ctx, s.metricsServer.Meter)\n\tif err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to create management app metrics: %w\", err)\n\t}\n\tmgmtServer.Inject[telemetry.AppMetrics](s.mgmtSrv, appMetrics)\n\n\tlog.Infof(\"Management server created\")\n\treturn nil\n}\n\nfunc (s *serverInstances) createSignalServer(ctx context.Context, cfg *CombinedConfig) error {\n\tif !cfg.Signal.Enabled {\n\t\treturn nil\n\t}\n\n\tvar err error\n\ts.signalSrv, err = signalServer.NewServer(ctx, s.metricsServer.Meter, \"signal_\")\n\tif err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to create signal server: %w\", err)\n\t}\n\n\tlog.Infof(\"Signal server created\")\n\treturn nil\n}\n\nfunc (s *serverInstances) createHealthcheckServer(cfg *CombinedConfig) error {\n\thCfg := healthcheck.Config{\n\t\tListenAddress: cfg.Server.HealthcheckAddress,\n\t\tServiceChecker: s.relaySrv,\n\t}\n\n\tvar err error\n\ts.healthcheck, err = createHealthCheck(hCfg, s.stunListeners)\n\treturn err\n}\n\n// setupServerHooks registers services with management's gRPC server.\nfunc setupServerHooks(servers *serverInstances, cfg *CombinedConfig) {\n\tif servers.mgmtSrv == nil {\n\t\treturn\n\t}\n\n\tservers.mgmtSrv.AfterInit(func(s *mgmtServer.BaseServer) {\n\t\tgrpcSrv := s.GRPCServer()\n\n\t\tif servers.signalSrv != nil {\n\t\t\tproto.RegisterSignalExchangeServer(grpcSrv, servers.signalSrv)\n\t\t\tlog.Infof(\"Signal server registered on port %s\", cfg.Server.ListenAddress)\n\t\t}\n\n\t\ts.SetHandlerFunc(createCombinedHandler(grpcSrv, s.APIHandler(), servers.relaySrv, servers.metricsServer.Meter, cfg))\n\t\tif servers.relaySrv != nil {\n\t\t\tlog.Infof(\"Relay WebSocket handler added (path: /relay)\")\n\t\t}\n\t})\n}\n\nfunc startServers(wg *sync.WaitGroup, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, metricsServer *sharedMetrics.Metrics) {\n\tif srv != nil {\n\t\tinstanceURL := srv.InstanceURL()\n\t\tlog.Infof(\"Relay server instance URL: %s\", instanceURL.String())\n\t\tlog.Infof(\"Relay WebSocket multiplexed on management port (no separate relay listener)\")\n\t}\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tlog.Infof(\"running metrics server: %s%s\", metricsServer.Addr, metricsServer.Endpoint)\n\t\tif err := metricsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {\n\t\t\tlog.Fatalf(\"failed to start metrics server: %v\", err)\n\t\t}\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tif err := httpHealthcheck.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {\n\t\t\tlog.Fatalf(\"failed to start healthcheck server: %v\", err)\n\t\t}\n\t}()\n\n\tif stunServer != nil {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tif err := stunServer.Listen(); err != nil {\n\t\t\t\tif errors.Is(err, stun.ErrServerClosed) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlog.Errorf(\"STUN server error: %v\", err)\n\t\t\t}\n\t\t}()\n\t}\n}\n\nfunc shutdownServers(ctx context.Context, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, mgmtSrv *mgmtServer.BaseServer, metricsServer *sharedMetrics.Metrics) error {\n\tvar errs error\n\n\tif err := httpHealthcheck.Shutdown(ctx); err != nil {\n\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close healthcheck server: %w\", err))\n\t}\n\n\tif stunServer != nil {\n\t\tif err := stunServer.Shutdown(); err != nil {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close STUN server: %w\", err))\n\t\t}\n\t}\n\n\tif srv != nil {\n\t\tif err := srv.Shutdown(ctx); err != nil {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close relay server: %w\", err))\n\t\t}\n\t}\n\n\tif mgmtSrv != nil {\n\t\tlog.Infof(\"shutting down management and signal servers\")\n\t\tif err := mgmtSrv.Stop(); err != nil {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close management server: %w\", err))\n\t\t}\n\t}\n\n\tif metricsServer != nil {\n\t\tlog.Infof(\"shutting down metrics server\")\n\t\tif err := metricsServer.Shutdown(ctx); err != nil {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close metrics server: %w\", err))\n\t\t}\n\t}\n\n\treturn errs\n}\n\nfunc createHealthCheck(hCfg healthcheck.Config, stunListeners []*net.UDPConn) (*healthcheck.Server, error) {\n\thttpHealthcheck, err := healthcheck.NewServer(hCfg)\n\tif err != nil {\n\t\tcleanupSTUNListeners(stunListeners)\n\t\treturn nil, fmt.Errorf(\"failed to create healthcheck server: %w\", err)\n\t}\n\treturn httpHealthcheck, nil\n}\n\nfunc createRelayServer(cfg relayServer.Config, stunListeners []*net.UDPConn) (*relayServer.Server, error) {\n\tsrv, err := relayServer.NewServer(cfg)\n\tif err != nil {\n\t\tcleanupSTUNListeners(stunListeners)\n\t\treturn nil, fmt.Errorf(\"failed to create relay server: %w\", err)\n\t}\n\treturn srv, nil\n}\n\nfunc cleanupSTUNListeners(stunListeners []*net.UDPConn) {\n\tfor _, l := range stunListeners {\n\t\t_ = l.Close()\n\t}\n}\n\nfunc createSTUNListeners(cfg *CombinedConfig) ([]*net.UDPConn, error) {\n\tvar stunListeners []*net.UDPConn\n\tif cfg.Relay.Stun.Enabled {\n\t\tfor _, port := range cfg.Relay.Stun.Ports {\n\t\t\tlistener, err := net.ListenUDP(\"udp\", &net.UDPAddr{Port: port})\n\t\t\tif err != nil {\n\t\t\t\tcleanupSTUNListeners(stunListeners)\n\t\t\t\treturn nil, fmt.Errorf(\"failed to create STUN listener on port %d: %w\", port, err)\n\t\t\t}\n\t\t\tstunListeners = append(stunListeners, listener)\n\t\t\tlog.Infof(\"STUN server listening on UDP port %d\", port)\n\t\t}\n\t}\n\treturn stunListeners, nil\n}\n\nfunc handleTLSConfig(cfg *CombinedConfig) (*tls.Config, bool, error) {\n\ttlsCfg := cfg.Server.TLS\n\n\tif tlsCfg.LetsEncrypt.AWSRoute53 {\n\t\tlog.Debugf(\"using Let's Encrypt DNS resolver with Route 53 support\")\n\t\tr53 := encryption.Route53TLS{\n\t\t\tDataDir: tlsCfg.LetsEncrypt.DataDir,\n\t\t\tEmail: tlsCfg.LetsEncrypt.Email,\n\t\t\tDomains: tlsCfg.LetsEncrypt.Domains,\n\t\t}\n\t\ttc, err := r53.GetCertificate()\n\t\tif err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\t\treturn tc, true, nil\n\t}\n\n\tif cfg.HasLetsEncrypt() {\n\t\tlog.Infof(\"setting up TLS with Let's Encrypt\")\n\t\tcertManager, err := encryption.CreateCertManager(tlsCfg.LetsEncrypt.DataDir, tlsCfg.LetsEncrypt.Domains...)\n\t\tif err != nil {\n\t\t\treturn nil, false, fmt.Errorf(\"failed creating LetsEncrypt cert manager: %w\", err)\n\t\t}\n\t\treturn certManager.TLSConfig(), true, nil\n\t}\n\n\tif cfg.HasTLSCert() {\n\t\tlog.Debugf(\"using file based TLS config\")\n\t\ttc, err := encryption.LoadTLSConfig(tlsCfg.CertFile, tlsCfg.KeyFile)\n\t\tif err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\t\treturn tc, true, nil\n\t}\n\n\treturn nil, false, nil\n}\n\nfunc createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (*mgmtServer.BaseServer, error) {\n\tmgmt := cfg.Management\n\n\tdnsDomain := mgmt.DnsDomain\n\tsingleAccModeDomain := dnsDomain\n\n\t// Extract port from listen address\n\t_, portStr, err := net.SplitHostPort(cfg.Server.ListenAddress)\n\tif err != nil {\n\t\t// If no port specified, assume default\n\t\tportStr = \"443\"\n\t}\n\tmgmtPort, _ := strconv.Atoi(portStr)\n\n\tmgmtSrv := mgmtServer.NewServer(\n\t\t&mgmtServer.Config{\n\t\t\tNbConfig: mgmtConfig,\n\t\t\tDNSDomain: dnsDomain,\n\t\t\tMgmtSingleAccModeDomain: singleAccModeDomain,\n\t\t\tMgmtPort: mgmtPort,\n\t\t\tMgmtMetricsPort: cfg.Server.MetricsPort,\n\t\t\tDisableMetrics: mgmt.DisableAnonymousMetrics,\n\t\t\tDisableGeoliteUpdate: mgmt.DisableGeoliteUpdate,\n\t\t\t// Always enable user deletion from IDP in combined server (embedded IdP is always enabled)\n\t\t\tUserDeleteFromIDPEnabled: true,\n\t\t},\n\t)\n\n\treturn mgmtSrv, nil\n}\n\n// createCombinedHandler creates an HTTP handler that multiplexes Management, Signal (via wsproxy), and Relay WebSocket traffic\nfunc createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, relaySrv *relayServer.Server, meter metric.Meter, cfg *CombinedConfig) http.Handler {\n\twsProxy := wsproxyserver.New(grpcServer, wsproxyserver.WithOTelMeter(meter))\n\n\tvar relayAcceptFn func(conn net.Conn)\n\tif relaySrv != nil {\n\t\trelayAcceptFn = relaySrv.RelayAccept()\n\t}\n\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch {\n\t\t// Native gRPC traffic (HTTP/2 with gRPC content-type)\n\t\tcase r.ProtoMajor == 2 && (strings.HasPrefix(r.Header.Get(\"Content-Type\"), \"application/grpc\") ||\n\t\t\tstrings.HasPrefix(r.Header.Get(\"Content-Type\"), \"application/grpc+proto\")):\n\t\t\tgrpcServer.ServeHTTP(w, r)\n\n\t\t// WebSocket proxy for Management gRPC\n\t\tcase r.URL.Path == wsproxy.ProxyPath+wsproxy.ManagementComponent:\n\t\t\twsProxy.Handler().ServeHTTP(w, r)\n\n\t\t// WebSocket proxy for Signal gRPC\n\t\tcase r.URL.Path == wsproxy.ProxyPath+wsproxy.SignalComponent:\n\t\t\tif cfg.Signal.Enabled {\n\t\t\t\twsProxy.Handler().ServeHTTP(w, r)\n\t\t\t} else {\n\t\t\t\thttp.Error(w, \"Signal service not enabled\", http.StatusNotFound)\n\t\t\t}\n\n\t\t// Relay WebSocket\n\t\tcase r.URL.Path == \"/relay\":\n\t\t\tif relayAcceptFn != nil {\n\t\t\t\thandleRelayWebSocket(w, r, relayAcceptFn, cfg)\n\t\t\t} else {\n\t\t\t\thttp.Error(w, \"Relay service not enabled\", http.StatusNotFound)\n\t\t\t}\n\n\t\t// Management HTTP API (default)\n\t\tdefault:\n\t\t\thttpHandler.ServeHTTP(w, r)\n\t\t}\n\t})\n}\n\n// handleRelayWebSocket handles incoming WebSocket connections for the relay service\nfunc handleRelayWebSocket(w http.ResponseWriter, r *http.Request, acceptFn func(conn net.Conn), cfg *CombinedConfig) {\n\tacceptOptions := &websocket.AcceptOptions{\n\t\tOriginPatterns: []string{\"*\"},\n\t}\n\n\twsConn, err := websocket.Accept(w, r, acceptOptions)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to accept relay ws connection: %s\", err)\n\t\treturn\n\t}\n\n\tconnRemoteAddr := r.RemoteAddr\n\tif r.Header.Get(\"X-Real-Ip\") != \"\" && r.Header.Get(\"X-Real-Port\") != \"\" {\n\t\tconnRemoteAddr = net.JoinHostPort(r.Header.Get(\"X-Real-Ip\"), r.Header.Get(\"X-Real-Port\"))\n\t}\n\n\trAddr, err := net.ResolveTCPAddr(\"tcp\", connRemoteAddr)\n\tif err != nil {\n\t\t_ = wsConn.Close(websocket.StatusInternalError, \"internal error\")\n\t\treturn\n\t}\n\n\tlAddr, err := net.ResolveTCPAddr(\"tcp\", cfg.Server.ListenAddress)\n\tif err != nil {\n\t\t_ = wsConn.Close(websocket.StatusInternalError, \"internal error\")\n\t\treturn\n\t}\n\n\tlog.Debugf(\"Relay WS client connected from: %s\", rAddr)\n\n\tconn := ws.NewConn(wsConn, lAddr, rAddr)\n\tacceptFn(conn)\n}\n\n// logConfig prints all configuration parameters for debugging\nfunc logConfig(cfg *CombinedConfig) {\n\tlog.Info(\"=== Configuration ===\")\n\tlogServerConfig(cfg)\n\tlogComponentsConfig(cfg)\n\tlogRelayConfig(cfg)\n\tlogManagementConfig(cfg)\n\tlog.Info(\"=== End Configuration ===\")\n}\n\nfunc logServerConfig(cfg *CombinedConfig) {\n\tlog.Info(\"--- Server ---\")\n\tlog.Infof(\" Listen address: %s\", cfg.Server.ListenAddress)\n\tlog.Infof(\" Exposed address: %s\", cfg.Server.ExposedAddress)\n\tlog.Infof(\" Healthcheck address: %s\", cfg.Server.HealthcheckAddress)\n\tlog.Infof(\" Metrics port: %d\", cfg.Server.MetricsPort)\n\tlog.Infof(\" Log level: %s\", cfg.Server.LogLevel)\n\tlog.Infof(\" Data dir: %s\", cfg.Server.DataDir)\n\n\tswitch {\n\tcase cfg.HasTLSCert():\n\t\tlog.Infof(\" TLS: cert=%s, key=%s\", cfg.Server.TLS.CertFile, cfg.Server.TLS.KeyFile)\n\tcase cfg.HasLetsEncrypt():\n\t\tlog.Infof(\" TLS: Let's Encrypt (domains=%v)\", cfg.Server.TLS.LetsEncrypt.Domains)\n\tdefault:\n\t\tlog.Info(\" TLS: disabled (using reverse proxy)\")\n\t}\n}\n\nfunc logComponentsConfig(cfg *CombinedConfig) {\n\tlog.Info(\"--- Components ---\")\n\tlog.Infof(\" Management: %v (log level: %s)\", cfg.Management.Enabled, cfg.Management.LogLevel)\n\tlog.Infof(\" Signal: %v (log level: %s)\", cfg.Signal.Enabled, cfg.Signal.LogLevel)\n\tlog.Infof(\" Relay: %v (log level: %s)\", cfg.Relay.Enabled, cfg.Relay.LogLevel)\n}\n\nfunc logRelayConfig(cfg *CombinedConfig) {\n\tif !cfg.Relay.Enabled {\n\t\treturn\n\t}\n\tlog.Info(\"--- Relay ---\")\n\tlog.Infof(\" Exposed address: %s\", cfg.Relay.ExposedAddress)\n\tlog.Infof(\" Auth secret: %s...\", maskSecret(cfg.Relay.AuthSecret))\n\tif cfg.Relay.Stun.Enabled {\n\t\tlog.Infof(\" STUN ports: %v (log level: %s)\", cfg.Relay.Stun.Ports, cfg.Relay.Stun.LogLevel)\n\t} else {\n\t\tlog.Info(\" STUN: disabled\")\n\t}\n}\n\nfunc logManagementConfig(cfg *CombinedConfig) {\n\tif !cfg.Management.Enabled {\n\t\treturn\n\t}\n\tlog.Info(\"--- Management ---\")\n\tlog.Infof(\" Data dir: %s\", cfg.Management.DataDir)\n\tlog.Infof(\" DNS domain: %s\", cfg.Management.DnsDomain)\n\tlog.Infof(\" Store engine: %s\", cfg.Management.Store.Engine)\n\tif cfg.Server.Store.DSN != \"\" {\n\t\tlog.Infof(\" Store DSN: %s\", maskDSNPassword(cfg.Server.Store.DSN))\n\t}\n\n\tlog.Info(\" Auth (embedded IdP):\")\n\tlog.Infof(\" Issuer: %s\", cfg.Management.Auth.Issuer)\n\tlog.Infof(\" Dashboard redirect URIs: %v\", cfg.Management.Auth.DashboardRedirectURIs)\n\tlog.Infof(\" CLI redirect URIs: %v\", cfg.Management.Auth.CLIRedirectURIs)\n\n\tlog.Info(\" Client settings:\")\n\tlog.Infof(\" Signal URI: %s\", cfg.Management.SignalURI)\n\tfor _, s := range cfg.Management.Stuns {\n\t\tlog.Infof(\" STUN: %s\", s.URI)\n\t}\n\tif len(cfg.Management.Relays.Addresses) > 0 {\n\t\tlog.Infof(\" Relay addresses: %v\", cfg.Management.Relays.Addresses)\n\t\tlog.Infof(\" Relay credentials TTL: %s\", cfg.Management.Relays.CredentialsTTL)\n\t}\n}\n\n// logEnvVars logs all NB_ environment variables that are currently set\nfunc logEnvVars() {\n\tlog.Info(\"=== Environment Variables ===\")\n\tfound := false\n\tfor _, env := range os.Environ() {\n\t\tif strings.HasPrefix(env, \"NB_\") {\n\t\t\tkey, _, _ := strings.Cut(env, \"=\")\n\t\t\tvalue := os.Getenv(key)\n\t\t\tkeyLower := strings.ToLower(key)\n\t\t\tif strings.Contains(keyLower, \"secret\") || strings.Contains(keyLower, \"key\") || strings.Contains(keyLower, \"password\") {\n\t\t\t\tvalue = maskSecret(value)\n\t\t\t} else if strings.Contains(keyLower, \"dsn\") {\n\t\t\t\tvalue = maskDSNPassword(value)\n\t\t\t}\n\t\t\tlog.Infof(\" %s=%s\", key, value)\n\t\t\tfound = true\n\t\t}\n\t}\n\tif !found {\n\t\tlog.Info(\" (none set)\")\n\t}\n\tlog.Info(\"=== End Environment Variables ===\")\n}\n\n// maskDSNPassword masks the password in a DSN string.\n// Handles both key=value format (\"password=secret\") and URI format (\"user:secret@host\").\nfunc maskDSNPassword(dsn string) string {\n\t// Key=value format: \"host=localhost user=nb password=secret dbname=nb\"\n\tif strings.Contains(dsn, \"password=\") {\n\t\tparts := strings.Fields(dsn)\n\t\tfor i, p := range parts {\n\t\t\tif strings.HasPrefix(p, \"password=\") {\n\t\t\t\tparts[i] = \"password=****\"\n\t\t\t}\n\t\t}\n\t\treturn strings.Join(parts, \" \")\n\t}\n\n\t// URI format: \"user:password@host...\"\n\tif atIdx := strings.Index(dsn, \"@\"); atIdx != -1 {\n\t\tprefix := dsn[:atIdx]\n\t\tif colonIdx := strings.Index(prefix, \":\"); colonIdx != -1 {\n\t\t\treturn prefix[:colonIdx+1] + \"****\" + dsn[atIdx:]\n\t\t}\n\t}\n\n\treturn dsn\n}\n\n// maskSecret returns first 4 chars of secret followed by \"...\"\nfunc maskSecret(secret string) string {\n\tif len(secret) <= 4 {\n\t\treturn \"****\"\n\t}\n\treturn secret[:4] + \"...\"\n}","originalFile":"package cmd\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/coder/websocket\"\n\t\"github.com/hashicorp/go-multierror\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"go.opentelemetry.io/otel/metric\"\n\t\"google.golang.org/grpc\"\n\n\t\"github.com/netbirdio/netbird/encryption\"\n\tmgmtServer \"github.com/netbirdio/netbird/management/internals/server\"\n\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n\t\"github.com/netbirdio/netbird/management/server/telemetry\"\n\t\"github.com/netbirdio/netbird/relay/healthcheck\"\n\trelayServer \"github.com/netbirdio/netbird/relay/server\"\n\t\"github.com/netbirdio/netbird/relay/server/listener/ws\"\n\tsharedMetrics \"github.com/netbirdio/netbird/shared/metrics\"\n\t\"github.com/netbirdio/netbird/shared/relay/auth\"\n\t\"github.com/netbirdio/netbird/shared/signal/proto\"\n\tsignalServer \"github.com/netbirdio/netbird/signal/server\"\n\t\"github.com/netbirdio/netbird/stun\"\n\t\"github.com/netbirdio/netbird/util\"\n\t\"github.com/netbirdio/netbird/util/wsproxy\"\n\twsproxyserver \"github.com/netbirdio/netbird/util/wsproxy/server\"\n)\n\nvar (\n\tconfigPath string\n\tconfig *CombinedConfig\n\n\trootCmd = &cobra.Command{\n\t\tUse: \"combined\",\n\t\tShort: \"Combined Netbird server (Management + Signal + Relay + STUN)\",\n\t\tLong: `Combined Netbird server for self-hosted deployments.\n\nAll services (Management, Signal, Relay) are multiplexed on a single port.\nOptional STUN server runs on separate UDP ports.\n\nConfiguration is loaded from a YAML file specified with --config.`,\n\t\tSilenceUsage: true,\n\t\tSilenceErrors: true,\n\t\tRunE: execute,\n\t}\n)\n\nfunc init() {\n\trootCmd.PersistentFlags().StringVarP(&configPath, \"config\", \"c\", \"\", \"path to YAML configuration file (required)\")\n\t_ = rootCmd.MarkPersistentFlagRequired(\"config\")\n\n\trootCmd.AddCommand(newTokenCommands())\n}\n\nfunc Execute() error {\n\treturn rootCmd.Execute()\n}\n\nfunc waitForExitSignal() {\n\tosSigs := make(chan os.Signal, 1)\n\tsignal.Notify(osSigs, syscall.SIGINT, syscall.SIGTERM)\n\t<-osSigs\n}\n\nfunc execute(cmd *cobra.Command, _ []string) error {\n\tif err := initializeConfig(); err != nil {\n\t\treturn err\n\t}\n\n\t// Management is required as the base server when signal or relay are enabled\n\tif (config.Signal.Enabled || config.Relay.Enabled) && !config.Management.Enabled {\n\t\treturn fmt.Errorf(\"management must be enabled when signal or relay are enabled (provides the base HTTP server)\")\n\t}\n\n\tservers, err := createAllServers(cmd.Context(), config)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Register services with management's gRPC server using AfterInit hook\n\tsetupServerHooks(servers, config)\n\n\t// Start management server (this also starts the HTTP listener)\n\tif servers.mgmtSrv != nil {\n\t\tif err := servers.mgmtSrv.Start(cmd.Context()); err != nil {\n\t\t\tcleanupSTUNListeners(servers.stunListeners)\n\t\t\treturn fmt.Errorf(\"failed to start management server: %w\", err)\n\t\t}\n\t}\n\n\t// Start all other servers\n\twg := sync.WaitGroup{}\n\tstartServers(&wg, servers.relaySrv, servers.healthcheck, servers.stunServer, servers.metricsServer)\n\n\twaitForExitSignal()\n\n\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\tdefer cancel()\n\n\terr = shutdownServers(ctx, servers.relaySrv, servers.healthcheck, servers.stunServer, servers.mgmtSrv, servers.metricsServer)\n\twg.Wait()\n\treturn err\n}\n\n// initializeConfig loads and validates the configuration, then initializes logging.\nfunc initializeConfig() error {\n\tvar err error\n\tconfig, err = LoadConfig(configPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to load config: %w\", err)\n\t}\n\n\tif err := config.Validate(); err != nil {\n\t\treturn fmt.Errorf(\"invalid config: %w\", err)\n\t}\n\n\tif err := util.InitLog(config.Server.LogLevel, config.Server.LogFile); err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize log: %w\", err)\n\t}\n\n\tif dsn := config.Server.Store.DSN; dsn != \"\" {\n\t\tswitch strings.ToLower(config.Server.Store.Engine) {\n\t\tcase \"postgres\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n\t\tcase \"mysql\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n\t\t}\n\t}\n\n\tif engine := config.Server.ActivityStore.Engine; engine != \"\" {\n\t\tengineLower := strings.ToLower(engine)\n\t\tif engineLower == \"postgres\" && config.Server.ActivityStore.DSN == \"\" {\n\t\t\treturn fmt.Errorf(\"activityStore.dsn is required when activityStore.engine is postgres\")\n\t\t}\n\t\tos.Setenv(\"NB_ACTIVITY_EVENT_STORE_ENGINE\", engineLower)\n\t\tif dsn := config.Server.ActivityStore.DSN; dsn != \"\" {\n\t\t\tos.Setenv(\"NB_ACTIVITY_EVENT_POSTGRES_DSN\", dsn)\n\t\t}\n\t}\n\n\tlog.Infof(\"Starting combined NetBird server\")\n\tlogConfig(config)\n\tlogEnvVars()\n\treturn nil\n}\n\n// serverInstances holds all server instances created during startup.\ntype serverInstances struct {\n\trelaySrv *relayServer.Server\n\tmgmtSrv *mgmtServer.BaseServer\n\tsignalSrv *signalServer.Server\n\thealthcheck *healthcheck.Server\n\tstunServer *stun.Server\n\tstunListeners []*net.UDPConn\n\tmetricsServer *sharedMetrics.Metrics\n}\n\n// createAllServers creates all server instances based on configuration.\nfunc createAllServers(ctx context.Context, cfg *CombinedConfig) (*serverInstances, error) {\n\tmetricsServer, err := sharedMetrics.NewServer(cfg.Server.MetricsPort, \"\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create metrics server: %w\", err)\n\t}\n\tservers := &serverInstances{\n\t\tmetricsServer: metricsServer,\n\t}\n\n\t_, tlsSupport, err := handleTLSConfig(cfg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to setup TLS config: %w\", err)\n\t}\n\n\tif err := servers.createRelayServer(cfg, tlsSupport); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := servers.createManagementServer(ctx, cfg); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := servers.createSignalServer(ctx, cfg); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := servers.createHealthcheckServer(cfg); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn servers, nil\n}\n\nfunc (s *serverInstances) createRelayServer(cfg *CombinedConfig, tlsSupport bool) error {\n\tif !cfg.Relay.Enabled {\n\t\treturn nil\n\t}\n\n\tvar err error\n\ts.stunListeners, err = createSTUNListeners(cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thashedSecret := sha256.Sum256([]byte(cfg.Relay.AuthSecret))\n\tauthenticator := auth.NewTimedHMACValidator(hashedSecret[:], 24*time.Hour)\n\n\trelayCfg := relayServer.Config{\n\t\tMeter: s.metricsServer.Meter,\n\t\tExposedAddress: cfg.Relay.ExposedAddress,\n\t\tAuthValidator: authenticator,\n\t\tTLSSupport: tlsSupport,\n\t}\n\n\ts.relaySrv, err = createRelayServer(relayCfg, s.stunListeners)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlog.Infof(\"Relay server created\")\n\n\tif len(s.stunListeners) > 0 {\n\t\ts.stunServer = stun.NewServer(s.stunListeners, cfg.Relay.Stun.LogLevel)\n\t}\n\n\treturn nil\n}\n\nfunc (s *serverInstances) createManagementServer(ctx context.Context, cfg *CombinedConfig) error {\n\tif !cfg.Management.Enabled {\n\t\treturn nil\n\t}\n\n\tmgmtConfig, err := cfg.ToManagementConfig()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create management config: %w\", err)\n\t}\n\n\t_, portStr, portErr := net.SplitHostPort(cfg.Server.ListenAddress)\n\tif portErr != nil {\n\t\tportStr = \"443\"\n\t}\n\tmgmtPort, _ := strconv.Atoi(portStr)\n\n\tif err := ApplyEmbeddedIdPConfig(ctx, mgmtConfig, mgmtPort, false); err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to apply embedded IdP config: %w\", err)\n\t}\n\n\tif err := EnsureEncryptionKey(ctx, mgmtConfig); err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to ensure encryption key: %w\", err)\n\t}\n\n\tLogConfigInfo(mgmtConfig)\n\n\ts.mgmtSrv, err = createManagementServer(cfg, mgmtConfig)\n\tif err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to create management server: %w\", err)\n\t}\n\n\t// Inject externally-managed AppMetrics so management uses the shared metrics server\n\tappMetrics, err := telemetry.NewAppMetricsWithMeter(ctx, s.metricsServer.Meter)\n\tif err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to create management app metrics: %w\", err)\n\t}\n\tmgmtServer.Inject[telemetry.AppMetrics](s.mgmtSrv, appMetrics)\n\n\tlog.Infof(\"Management server created\")\n\treturn nil\n}\n\nfunc (s *serverInstances) createSignalServer(ctx context.Context, cfg *CombinedConfig) error {\n\tif !cfg.Signal.Enabled {\n\t\treturn nil\n\t}\n\n\tvar err error\n\ts.signalSrv, err = signalServer.NewServer(ctx, s.metricsServer.Meter, \"signal_\")\n\tif err != nil {\n\t\tcleanupSTUNListeners(s.stunListeners)\n\t\treturn fmt.Errorf(\"failed to create signal server: %w\", err)\n\t}\n\n\tlog.Infof(\"Signal server created\")\n\treturn nil\n}\n\nfunc (s *serverInstances) createHealthcheckServer(cfg *CombinedConfig) error {\n\thCfg := healthcheck.Config{\n\t\tListenAddress: cfg.Server.HealthcheckAddress,\n\t\tServiceChecker: s.relaySrv,\n\t}\n\n\tvar err error\n\ts.healthcheck, err = createHealthCheck(hCfg, s.stunListeners)\n\treturn err\n}\n\n// setupServerHooks registers services with management's gRPC server.\nfunc setupServerHooks(servers *serverInstances, cfg *CombinedConfig) {\n\tif servers.mgmtSrv == nil {\n\t\treturn\n\t}\n\n\tservers.mgmtSrv.AfterInit(func(s *mgmtServer.BaseServer) {\n\t\tgrpcSrv := s.GRPCServer()\n\n\t\tif servers.signalSrv != nil {\n\t\t\tproto.RegisterSignalExchangeServer(grpcSrv, servers.signalSrv)\n\t\t\tlog.Infof(\"Signal server registered on port %s\", cfg.Server.ListenAddress)\n\t\t}\n\n\t\ts.SetHandlerFunc(createCombinedHandler(grpcSrv, s.APIHandler(), servers.relaySrv, servers.metricsServer.Meter, cfg))\n\t\tif servers.relaySrv != nil {\n\t\t\tlog.Infof(\"Relay WebSocket handler added (path: /relay)\")\n\t\t}\n\t})\n}\n\nfunc startServers(wg *sync.WaitGroup, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, metricsServer *sharedMetrics.Metrics) {\n\tif srv != nil {\n\t\tinstanceURL := srv.InstanceURL()\n\t\tlog.Infof(\"Relay server instance URL: %s\", instanceURL.String())\n\t\tlog.Infof(\"Relay WebSocket multiplexed on management port (no separate relay listener)\")\n\t}\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tlog.Infof(\"running metrics server: %s%s\", metricsServer.Addr, metricsServer.Endpoint)\n\t\tif err := metricsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {\n\t\t\tlog.Fatalf(\"failed to start metrics server: %v\", err)\n\t\t}\n\t}()\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tif err := httpHealthcheck.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {\n\t\t\tlog.Fatalf(\"failed to start healthcheck server: %v\", err)\n\t\t}\n\t}()\n\n\tif stunServer != nil {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tif err := stunServer.Listen(); err != nil {\n\t\t\t\tif errors.Is(err, stun.ErrServerClosed) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlog.Errorf(\"STUN server error: %v\", err)\n\t\t\t}\n\t\t}()\n\t}\n}\n\nfunc shutdownServers(ctx context.Context, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, mgmtSrv *mgmtServer.BaseServer, metricsServer *sharedMetrics.Metrics) error {\n\tvar errs error\n\n\tif err := httpHealthcheck.Shutdown(ctx); err != nil {\n\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close healthcheck server: %w\", err))\n\t}\n\n\tif stunServer != nil {\n\t\tif err := stunServer.Shutdown(); err != nil {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close STUN server: %w\", err))\n\t\t}\n\t}\n\n\tif srv != nil {\n\t\tif err := srv.Shutdown(ctx); err != nil {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close relay server: %w\", err))\n\t\t}\n\t}\n\n\tif mgmtSrv != nil {\n\t\tlog.Infof(\"shutting down management and signal servers\")\n\t\tif err := mgmtSrv.Stop(); err != nil {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close management server: %w\", err))\n\t\t}\n\t}\n\n\tif metricsServer != nil {\n\t\tlog.Infof(\"shutting down metrics server\")\n\t\tif err := metricsServer.Shutdown(ctx); err != nil {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"failed to close metrics server: %w\", err))\n\t\t}\n\t}\n\n\treturn errs\n}\n\nfunc createHealthCheck(hCfg healthcheck.Config, stunListeners []*net.UDPConn) (*healthcheck.Server, error) {\n\thttpHealthcheck, err := healthcheck.NewServer(hCfg)\n\tif err != nil {\n\t\tcleanupSTUNListeners(stunListeners)\n\t\treturn nil, fmt.Errorf(\"failed to create healthcheck server: %w\", err)\n\t}\n\treturn httpHealthcheck, nil\n}\n\nfunc createRelayServer(cfg relayServer.Config, stunListeners []*net.UDPConn) (*relayServer.Server, error) {\n\tsrv, err := relayServer.NewServer(cfg)\n\tif err != nil {\n\t\tcleanupSTUNListeners(stunListeners)\n\t\treturn nil, fmt.Errorf(\"failed to create relay server: %w\", err)\n\t}\n\treturn srv, nil\n}\n\nfunc cleanupSTUNListeners(stunListeners []*net.UDPConn) {\n\tfor _, l := range stunListeners {\n\t\t_ = l.Close()\n\t}\n}\n\nfunc createSTUNListeners(cfg *CombinedConfig) ([]*net.UDPConn, error) {\n\tvar stunListeners []*net.UDPConn\n\tif cfg.Relay.Stun.Enabled {\n\t\tfor _, port := range cfg.Relay.Stun.Ports {\n\t\t\tlistener, err := net.ListenUDP(\"udp\", &net.UDPAddr{Port: port})\n\t\t\tif err != nil {\n\t\t\t\tcleanupSTUNListeners(stunListeners)\n\t\t\t\treturn nil, fmt.Errorf(\"failed to create STUN listener on port %d: %w\", port, err)\n\t\t\t}\n\t\t\tstunListeners = append(stunListeners, listener)\n\t\t\tlog.Infof(\"STUN server listening on UDP port %d\", port)\n\t\t}\n\t}\n\treturn stunListeners, nil\n}\n\nfunc handleTLSConfig(cfg *CombinedConfig) (*tls.Config, bool, error) {\n\ttlsCfg := cfg.Server.TLS\n\n\tif tlsCfg.LetsEncrypt.AWSRoute53 {\n\t\tlog.Debugf(\"using Let's Encrypt DNS resolver with Route 53 support\")\n\t\tr53 := encryption.Route53TLS{\n\t\t\tDataDir: tlsCfg.LetsEncrypt.DataDir,\n\t\t\tEmail: tlsCfg.LetsEncrypt.Email,\n\t\t\tDomains: tlsCfg.LetsEncrypt.Domains,\n\t\t}\n\t\ttc, err := r53.GetCertificate()\n\t\tif err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\t\treturn tc, true, nil\n\t}\n\n\tif cfg.HasLetsEncrypt() {\n\t\tlog.Infof(\"setting up TLS with Let's Encrypt\")\n\t\tcertManager, err := encryption.CreateCertManager(tlsCfg.LetsEncrypt.DataDir, tlsCfg.LetsEncrypt.Domains...)\n\t\tif err != nil {\n\t\t\treturn nil, false, fmt.Errorf(\"failed creating LetsEncrypt cert manager: %w\", err)\n\t\t}\n\t\treturn certManager.TLSConfig(), true, nil\n\t}\n\n\tif cfg.HasTLSCert() {\n\t\tlog.Debugf(\"using file based TLS config\")\n\t\ttc, err := encryption.LoadTLSConfig(tlsCfg.CertFile, tlsCfg.KeyFile)\n\t\tif err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\t\treturn tc, true, nil\n\t}\n\n\treturn nil, false, nil\n}\n\nfunc createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (*mgmtServer.BaseServer, error) {\n\tmgmt := cfg.Management\n\n\tdnsDomain := mgmt.DnsDomain\n\tsingleAccModeDomain := dnsDomain\n\n\t// Extract port from listen address\n\t_, portStr, err := net.SplitHostPort(cfg.Server.ListenAddress)\n\tif err != nil {\n\t\t// If no port specified, assume default\n\t\tportStr = \"443\"\n\t}\n\tmgmtPort, _ := strconv.Atoi(portStr)\n\n\tmgmtSrv := mgmtServer.NewServer(\n\t\t&mgmtServer.Config{\n\t\t\tNbConfig: mgmtConfig,\n\t\t\tDNSDomain: dnsDomain,\n\t\t\tMgmtSingleAccModeDomain: singleAccModeDomain,\n\t\t\tMgmtPort: mgmtPort,\n\t\t\tMgmtMetricsPort: cfg.Server.MetricsPort,\n\t\t\tDisableMetrics: mgmt.DisableAnonymousMetrics,\n\t\t\tDisableGeoliteUpdate: mgmt.DisableGeoliteUpdate,\n\t\t\t// Always enable user deletion from IDP in combined server (embedded IdP is always enabled)\n\t\t\tUserDeleteFromIDPEnabled: true,\n\t\t},\n\t)\n\n\treturn mgmtSrv, nil\n}\n\n// createCombinedHandler creates an HTTP handler that multiplexes Management, Signal (via wsproxy), and Relay WebSocket traffic\nfunc createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, relaySrv *relayServer.Server, meter metric.Meter, cfg *CombinedConfig) http.Handler {\n\twsProxy := wsproxyserver.New(grpcServer, wsproxyserver.WithOTelMeter(meter))\n\n\tvar relayAcceptFn func(conn net.Conn)\n\tif relaySrv != nil {\n\t\trelayAcceptFn = relaySrv.RelayAccept()\n\t}\n\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch {\n\t\t// Native gRPC traffic (HTTP/2 with gRPC content-type)\n\t\tcase r.ProtoMajor == 2 && (strings.HasPrefix(r.Header.Get(\"Content-Type\"), \"application/grpc\") ||\n\t\t\tstrings.HasPrefix(r.Header.Get(\"Content-Type\"), \"application/grpc+proto\")):\n\t\t\tgrpcServer.ServeHTTP(w, r)\n\n\t\t// WebSocket proxy for Management gRPC\n\t\tcase r.URL.Path == wsproxy.ProxyPath+wsproxy.ManagementComponent:\n\t\t\twsProxy.Handler().ServeHTTP(w, r)\n\n\t\t// WebSocket proxy for Signal gRPC\n\t\tcase r.URL.Path == wsproxy.ProxyPath+wsproxy.SignalComponent:\n\t\t\tif cfg.Signal.Enabled {\n\t\t\t\twsProxy.Handler().ServeHTTP(w, r)\n\t\t\t} else {\n\t\t\t\thttp.Error(w, \"Signal service not enabled\", http.StatusNotFound)\n\t\t\t}\n\n\t\t// Relay WebSocket\n\t\tcase r.URL.Path == \"/relay\":\n\t\t\tif relayAcceptFn != nil {\n\t\t\t\thandleRelayWebSocket(w, r, relayAcceptFn, cfg)\n\t\t\t} else {\n\t\t\t\thttp.Error(w, \"Relay service not enabled\", http.StatusNotFound)\n\t\t\t}\n\n\t\t// Management HTTP API (default)\n\t\tdefault:\n\t\t\thttpHandler.ServeHTTP(w, r)\n\t\t}\n\t})\n}\n\n// handleRelayWebSocket handles incoming WebSocket connections for the relay service\nfunc handleRelayWebSocket(w http.ResponseWriter, r *http.Request, acceptFn func(conn net.Conn), cfg *CombinedConfig) {\n\tacceptOptions := &websocket.AcceptOptions{\n\t\tOriginPatterns: []string{\"*\"},\n\t}\n\n\twsConn, err := websocket.Accept(w, r, acceptOptions)\n\tif err != nil {\n\t\tlog.Errorf(\"failed to accept relay ws connection: %s\", err)\n\t\treturn\n\t}\n\n\tconnRemoteAddr := r.RemoteAddr\n\tif r.Header.Get(\"X-Real-Ip\") != \"\" && r.Header.Get(\"X-Real-Port\") != \"\" {\n\t\tconnRemoteAddr = net.JoinHostPort(r.Header.Get(\"X-Real-Ip\"), r.Header.Get(\"X-Real-Port\"))\n\t}\n\n\trAddr, err := net.ResolveTCPAddr(\"tcp\", connRemoteAddr)\n\tif err != nil {\n\t\t_ = wsConn.Close(websocket.StatusInternalError, \"internal error\")\n\t\treturn\n\t}\n\n\tlAddr, err := net.ResolveTCPAddr(\"tcp\", cfg.Server.ListenAddress)\n\tif err != nil {\n\t\t_ = wsConn.Close(websocket.StatusInternalError, \"internal error\")\n\t\treturn\n\t}\n\n\tlog.Debugf(\"Relay WS client connected from: %s\", rAddr)\n\n\tconn := ws.NewConn(wsConn, lAddr, rAddr)\n\tacceptFn(conn)\n}\n\n// logConfig prints all configuration parameters for debugging\nfunc logConfig(cfg *CombinedConfig) {\n\tlog.Info(\"=== Configuration ===\")\n\tlogServerConfig(cfg)\n\tlogComponentsConfig(cfg)\n\tlogRelayConfig(cfg)\n\tlogManagementConfig(cfg)\n\tlog.Info(\"=== End Configuration ===\")\n}\n\nfunc logServerConfig(cfg *CombinedConfig) {\n\tlog.Info(\"--- Server ---\")\n\tlog.Infof(\" Listen address: %s\", cfg.Server.ListenAddress)\n\tlog.Infof(\" Exposed address: %s\", cfg.Server.ExposedAddress)\n\tlog.Infof(\" Healthcheck address: %s\", cfg.Server.HealthcheckAddress)\n\tlog.Infof(\" Metrics port: %d\", cfg.Server.MetricsPort)\n\tlog.Infof(\" Log level: %s\", cfg.Server.LogLevel)\n\tlog.Infof(\" Data dir: %s\", cfg.Server.DataDir)\n\n\tswitch {\n\tcase cfg.HasTLSCert():\n\t\tlog.Infof(\" TLS: cert=%s, key=%s\", cfg.Server.TLS.CertFile, cfg.Server.TLS.KeyFile)\n\tcase cfg.HasLetsEncrypt():\n\t\tlog.Infof(\" TLS: Let's Encrypt (domains=%v)\", cfg.Server.TLS.LetsEncrypt.Domains)\n\tdefault:\n\t\tlog.Info(\" TLS: disabled (using reverse proxy)\")\n\t}\n}\n\nfunc logComponentsConfig(cfg *CombinedConfig) {\n\tlog.Info(\"--- Components ---\")\n\tlog.Infof(\" Management: %v (log level: %s)\", cfg.Management.Enabled, cfg.Management.LogLevel)\n\tlog.Infof(\" Signal: %v (log level: %s)\", cfg.Signal.Enabled, cfg.Signal.LogLevel)\n\tlog.Infof(\" Relay: %v (log level: %s)\", cfg.Relay.Enabled, cfg.Relay.LogLevel)\n}\n\nfunc logRelayConfig(cfg *CombinedConfig) {\n\tif !cfg.Relay.Enabled {\n\t\treturn\n\t}\n\tlog.Info(\"--- Relay ---\")\n\tlog.Infof(\" Exposed address: %s\", cfg.Relay.ExposedAddress)\n\tlog.Infof(\" Auth secret: %s...\", maskSecret(cfg.Relay.AuthSecret))\n\tif cfg.Relay.Stun.Enabled {\n\t\tlog.Infof(\" STUN ports: %v (log level: %s)\", cfg.Relay.Stun.Ports, cfg.Relay.Stun.LogLevel)\n\t} else {\n\t\tlog.Info(\" STUN: disabled\")\n\t}\n}\n\nfunc logManagementConfig(cfg *CombinedConfig) {\n\tif !cfg.Management.Enabled {\n\t\treturn\n\t}\n\tlog.Info(\"--- Management ---\")\n\tlog.Infof(\" Data dir: %s\", cfg.Management.DataDir)\n\tlog.Infof(\" DNS domain: %s\", cfg.Management.DnsDomain)\n\tlog.Infof(\" Store engine: %s\", cfg.Management.Store.Engine)\n\tif cfg.Server.Store.DSN != \"\" {\n\t\tlog.Infof(\" Store DSN: %s\", maskDSNPassword(cfg.Server.Store.DSN))\n\t}\n\n\tlog.Info(\" Auth (embedded IdP):\")\n\tlog.Infof(\" Issuer: %s\", cfg.Management.Auth.Issuer)\n\tlog.Infof(\" Dashboard redirect URIs: %v\", cfg.Management.Auth.DashboardRedirectURIs)\n\tlog.Infof(\" CLI redirect URIs: %v\", cfg.Management.Auth.CLIRedirectURIs)\n\n\tlog.Info(\" Client settings:\")\n\tlog.Infof(\" Signal URI: %s\", cfg.Management.SignalURI)\n\tfor _, s := range cfg.Management.Stuns {\n\t\tlog.Infof(\" STUN: %s\", s.URI)\n\t}\n\tif len(cfg.Management.Relays.Addresses) > 0 {\n\t\tlog.Infof(\" Relay addresses: %v\", cfg.Management.Relays.Addresses)\n\t\tlog.Infof(\" Relay credentials TTL: %s\", cfg.Management.Relays.CredentialsTTL)\n\t}\n}\n\n// logEnvVars logs all NB_ environment variables that are currently set\nfunc logEnvVars() {\n\tlog.Info(\"=== Environment Variables ===\")\n\tfound := false\n\tfor _, env := range os.Environ() {\n\t\tif strings.HasPrefix(env, \"NB_\") {\n\t\t\tkey, _, _ := strings.Cut(env, \"=\")\n\t\t\tvalue := os.Getenv(key)\n\t\t\tkeyLower := strings.ToLower(key)\n\t\t\tif strings.Contains(keyLower, \"secret\") || strings.Contains(keyLower, \"key\") || strings.Contains(keyLower, \"password\") {\n\t\t\t\tvalue = maskSecret(value)\n\t\t\t} else if strings.Contains(keyLower, \"dsn\") {\n\t\t\t\tvalue = maskDSNPassword(value)\n\t\t\t}\n\t\t\tlog.Infof(\" %s=%s\", key, value)\n\t\t\tfound = true\n\t\t}\n\t}\n\tif !found {\n\t\tlog.Info(\" (none set)\")\n\t}\n\tlog.Info(\"=== End Environment Variables ===\")\n}\n\n// maskDSNPassword masks the password in a DSN string.\n// Handles both key=value format (\"password=secret\") and URI format (\"user:secret@host\").\nfunc maskDSNPassword(dsn string) string {\n\t// Key=value format: \"host=localhost user=nb password=secret dbname=nb\"\n\tif strings.Contains(dsn, \"password=\") {\n\t\tparts := strings.Fields(dsn)\n\t\tfor i, p := range parts {\n\t\t\tif strings.HasPrefix(p, \"password=\") {\n\t\t\t\tparts[i] = \"password=****\"\n\t\t\t}\n\t\t}\n\t\treturn strings.Join(parts, \" \")\n\t}\n\n\t// URI format: \"user:password@host...\"\n\tif atIdx := strings.Index(dsn, \"@\"); atIdx != -1 {\n\t\tprefix := dsn[:atIdx]\n\t\tif colonIdx := strings.Index(prefix, \":\"); colonIdx != -1 {\n\t\t\treturn prefix[:colonIdx+1] + \"****\" + dsn[atIdx:]\n\t\t}\n\t}\n\n\treturn dsn\n}\n\n// maskSecret returns first 4 chars of secret followed by \"...\"\nfunc maskSecret(secret string) string {\n\tif len(secret) <= 4 {\n\t\treturn \"****\"\n\t}\n\treturn secret[:4] + \"...\"\n}\n","structuredPatch":[{"oldStart":140,"oldLines":6,"newStart":140,"newLines":9,"lines":[" os.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)"," }"," }","+ if file := config.Server.Store.File; file != \"\" {","+ os.Setenv(\"NB_STORE_ENGINE_SQLITE_FILE\", file)","+ }"," "," if engine := config.Server.ActivityStore.Engine; engine != \"\" {"," engineLower := strings.ToLower(engine)"]},{"oldStart":151,"oldLines":6,"newStart":154,"newLines":9,"lines":[" os.Setenv(\"NB_ACTIVITY_EVENT_POSTGRES_DSN\", dsn)"," }"," }","+ if file := config.Server.ActivityStore.File; file != \"\" {","+ os.Setenv(\"NB_ACTIVITY_EVENT_SQLITE_FILE\", file)","+ }"," "," log.Infof(\"Starting combined NetBird server\")"," logConfig(config)"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"aea0db76-4fc2-44fb-a18a-87e6326c1809"} +{"parentUuid":"b625d80d-c176-45ce-8f4c-145ae323b4c8","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01VVD7azzFvKDyEhBdKVtg5U","type":"message","role":"assistant","content":[{"type":"text","text":"Now update `token.go` to set the store file env var:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":794,"cache_read_input_tokens":82691,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":794},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYeoEaVKuDNwHD73pmPJ1","type":"assistant","uuid":"2489dced-6904-4192-b054-cef38390be89","timestamp":"2026-03-02T19:22:32.205Z"} +{"parentUuid":"2489dced-6904-4192-b054-cef38390be89","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01VVD7azzFvKDyEhBdKVtg5U","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Lop8Fncy2qirpahabRzKoS","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/token.go"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":794,"cache_read_input_tokens":82691,"output_tokens":94,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":794,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYeoEaVKuDNwHD73pmPJ1","type":"assistant","uuid":"a6e87bfe-1e08-4d26-939c-a0a0f7b45dfb","timestamp":"2026-03-02T19:22:32.383Z"} +{"parentUuid":"a6e87bfe-1e08-4d26-939c-a0a0f7b45dfb","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01Lop8Fncy2qirpahabRzKoS","toolUseID":"toolu_01Lop8Fncy2qirpahabRzKoS","timestamp":"2026-03-02T19:22:32.464Z","uuid":"239b4c9b-f014-46a2-aa7e-7ae32caf7352"} +{"parentUuid":"a6e87bfe-1e08-4d26-939c-a0a0f7b45dfb","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Lop8Fncy2qirpahabRzKoS","type":"tool_result","content":" 1→package cmd\n 2→\n 3→import (\n 4→\t\"context\"\n 5→\t\"fmt\"\n 6→\t\"os\"\n 7→\t\"strings\"\n 8→\n 9→\tlog \"github.com/sirupsen/logrus\"\n 10→\t\"github.com/spf13/cobra\"\n 11→\n 12→\t\"github.com/netbirdio/netbird/formatter/hook\"\n 13→\ttokencmd \"github.com/netbirdio/netbird/management/cmd/token\"\n 14→\t\"github.com/netbirdio/netbird/management/server/store\"\n 15→\t\"github.com/netbirdio/netbird/management/server/types\"\n 16→\t\"github.com/netbirdio/netbird/util\"\n 17→)\n 18→\n 19→// newTokenCommands creates the token command tree with combined-specific store opener.\n 20→func newTokenCommands() *cobra.Command {\n 21→\treturn tokencmd.NewCommands(withTokenStore)\n 22→}\n 23→\n 24→// withTokenStore loads the combined YAML config, initializes the store, and calls fn.\n 25→func withTokenStore(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error {\n 26→\tif err := util.InitLog(\"error\", \"console\"); err != nil {\n 27→\t\treturn fmt.Errorf(\"init log: %w\", err)\n 28→\t}\n 29→\n 30→\tctx := context.WithValue(cmd.Context(), hook.ExecutionContextKey, hook.SystemSource) //nolint:staticcheck\n 31→\n 32→\tcfg, err := LoadConfig(configPath)\n 33→\tif err != nil {\n 34→\t\treturn fmt.Errorf(\"load config: %w\", err)\n 35→\t}\n 36→\n 37→\tif dsn := cfg.Server.Store.DSN; dsn != \"\" {\n 38→\t\tswitch strings.ToLower(cfg.Server.Store.Engine) {\n 39→\t\tcase \"postgres\":\n 40→\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n 41→\t\tcase \"mysql\":\n 42→\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n 43→\t\t}\n 44→\t}\n 45→\n 46→\tdatadir := cfg.Management.DataDir\n 47→\tengine := types.Engine(cfg.Management.Store.Engine)\n 48→\n 49→\ts, err := store.NewStore(ctx, engine, datadir, nil, true)\n 50→\tif err != nil {\n 51→\t\treturn fmt.Errorf(\"create store: %w\", err)\n 52→\t}\n 53→\tdefer func() {\n 54→\t\tif err := s.Close(ctx); err != nil {\n 55→\t\t\tlog.Debugf(\"close store: %v\", err)\n 56→\t\t}\n 57→\t}()\n 58→\n 59→\treturn fn(ctx, s)\n 60→}\n 61→"}]},"uuid":"024ad5cb-ee5f-49d7-a6f1-79e598e28570","timestamp":"2026-03-02T19:22:32.463Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/token.go","content":"package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/netbirdio/netbird/formatter/hook\"\n\ttokencmd \"github.com/netbirdio/netbird/management/cmd/token\"\n\t\"github.com/netbirdio/netbird/management/server/store\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util\"\n)\n\n// newTokenCommands creates the token command tree with combined-specific store opener.\nfunc newTokenCommands() *cobra.Command {\n\treturn tokencmd.NewCommands(withTokenStore)\n}\n\n// withTokenStore loads the combined YAML config, initializes the store, and calls fn.\nfunc withTokenStore(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error {\n\tif err := util.InitLog(\"error\", \"console\"); err != nil {\n\t\treturn fmt.Errorf(\"init log: %w\", err)\n\t}\n\n\tctx := context.WithValue(cmd.Context(), hook.ExecutionContextKey, hook.SystemSource) //nolint:staticcheck\n\n\tcfg, err := LoadConfig(configPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"load config: %w\", err)\n\t}\n\n\tif dsn := cfg.Server.Store.DSN; dsn != \"\" {\n\t\tswitch strings.ToLower(cfg.Server.Store.Engine) {\n\t\tcase \"postgres\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n\t\tcase \"mysql\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n\t\t}\n\t}\n\n\tdatadir := cfg.Management.DataDir\n\tengine := types.Engine(cfg.Management.Store.Engine)\n\n\ts, err := store.NewStore(ctx, engine, datadir, nil, true)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create store: %w\", err)\n\t}\n\tdefer func() {\n\t\tif err := s.Close(ctx); err != nil {\n\t\t\tlog.Debugf(\"close store: %v\", err)\n\t\t}\n\t}()\n\n\treturn fn(ctx, s)\n}\n","numLines":61,"startLine":1,"totalLines":61}},"sourceToolAssistantUUID":"a6e87bfe-1e08-4d26-939c-a0a0f7b45dfb"} +{"parentUuid":"024ad5cb-ee5f-49d7-a6f1-79e598e28570","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01E3PZNfyJhuv72oHWW9ETo8","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01V44fpKCATrYCEmfHvAyP6o","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/token.go","old_string":"\tif dsn := cfg.Server.Store.DSN; dsn != \"\" {\n\t\tswitch strings.ToLower(cfg.Server.Store.Engine) {\n\t\tcase \"postgres\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n\t\tcase \"mysql\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n\t\t}\n\t}\n\n\tdatadir := cfg.Management.DataDir","new_string":"\tif dsn := cfg.Server.Store.DSN; dsn != \"\" {\n\t\tswitch strings.ToLower(cfg.Server.Store.Engine) {\n\t\tcase \"postgres\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n\t\tcase \"mysql\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n\t\t}\n\t}\n\tif file := cfg.Server.Store.File; file != \"\" {\n\t\tos.Setenv(\"NB_STORE_ENGINE_SQLITE_FILE\", file)\n\t}\n\n\tdatadir := cfg.Management.DataDir"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":902,"cache_read_input_tokens":83485,"output_tokens":408,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":902,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYeoEtEvUqPzTDqEyxvnp","type":"assistant","uuid":"b02f5764-88db-480f-8fd9-127cad3df2fa","timestamp":"2026-03-02T19:22:37.989Z"} +{"type":"file-history-snapshot","messageId":"b02f5764-88db-480f-8fd9-127cad3df2fa","snapshot":{"messageId":"7c71f5f8-6fbe-48a5-8d9b-9322a19b9593","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v2","version":2,"backupTime":"2026-03-02T19:15:31.656Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v2","version":2,"backupTime":"2026-03-02T19:20:06.400Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v1","version":1,"backupTime":"2026-03-02T19:21:36.151Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v1","version":1,"backupTime":"2026-03-02T19:22:27.975Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v1","version":1,"backupTime":"2026-03-02T19:23:23.859Z"}},"timestamp":"2026-03-02T19:20:06.399Z"},"isSnapshotUpdate":true} +{"parentUuid":"b02f5764-88db-480f-8fd9-127cad3df2fa","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_01V44fpKCATrYCEmfHvAyP6o","toolUseID":"toolu_01V44fpKCATrYCEmfHvAyP6o","timestamp":"2026-03-02T19:23:23.866Z","uuid":"332a7f4d-eeb3-4940-8ba6-2451f3f17fd2"} +{"parentUuid":"b02f5764-88db-480f-8fd9-127cad3df2fa","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01V44fpKCATrYCEmfHvAyP6o","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/token.go has been updated successfully."}]},"uuid":"8314730c-c5b8-45a3-8a53-36f1fcbe311a","timestamp":"2026-03-02T19:23:23.864Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/token.go","oldString":"package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/netbirdio/netbird/formatter/hook\"\n\ttokencmd \"github.com/netbirdio/netbird/management/cmd/token\"\n\t\"github.com/netbirdio/netbird/management/server/store\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util\"\n)\n\n// newTokenCommands creates the token command tree with combined-specific store opener.\nfunc newTokenCommands() *cobra.Command {\n\treturn tokencmd.NewCommands(withTokenStore)\n}\n\n// withTokenStore loads the combined YAML config, initializes the store, and calls fn.\nfunc withTokenStore(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error {\n\tif err := util.InitLog(\"error\", \"console\"); err != nil {\n\t\treturn fmt.Errorf(\"init log: %w\", err)\n\t}\n\n\tctx := context.WithValue(cmd.Context(), hook.ExecutionContextKey, hook.SystemSource) //nolint:staticcheck\n\n\tcfg, err := LoadConfig(configPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"load config: %w\", err)\n\t}\n\n\tif dsn := cfg.Server.Store.DSN; dsn != \"\" {\n\t\tswitch strings.ToLower(cfg.Server.Store.Engine) {\n\t\tcase \"postgres\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n\t\tcase \"mysql\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n\t\t}\n\t}\n\n\tdatadir := cfg.Management.DataDir\n\tengine := types.Engine(cfg.Management.Store.Engine)\n\n\ts, err := store.NewStore(ctx, engine, datadir, nil, true)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create store: %w\", err)\n\t}\n\tdefer func() {\n\t\tif err := s.Close(ctx); err != nil {\n\t\t\tlog.Debugf(\"close store: %v\", err)\n\t\t}\n\t}()\n\n\treturn fn(ctx, s)\n}","newString":"package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/netbirdio/netbird/formatter/hook\"\n\ttokencmd \"github.com/netbirdio/netbird/management/cmd/token\"\n\t\"github.com/netbirdio/netbird/management/server/store\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util\"\n)\n\n// newTokenCommands creates the token command tree with combined-specific store opener.\nfunc newTokenCommands() *cobra.Command {\n\treturn tokencmd.NewCommands(withTokenStore)\n}\n\n// withTokenStore loads the combined YAML config, initializes the store, and calls fn.\nfunc withTokenStore(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error {\n\tif err := util.InitLog(\"error\", \"console\"); err != nil {\n\t\treturn fmt.Errorf(\"init log: %w\", err)\n\t}\n\n\tctx := context.WithValue(cmd.Context(), hook.ExecutionContextKey, hook.SystemSource) //nolint:staticcheck\n\n\tcfg, err := LoadConfig(configPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"load config: %w\", err)\n\t}\n\n\tif dsn := cfg.Server.Store.DSN; dsn != \"\" {\n\t\tswitch strings.ToLower(cfg.Server.Store.Engine) {\n\t\tcase \"postgres\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n\t\tcase \"mysql\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n\t\t}\n\t}\n\tif file := cfg.Server.Store.File; file != \"\" {\n\t\tos.Setenv(\"NB_STORE_ENGINE_SQLITE_FILE\", file)\n\t}\n\n\tdatadir := cfg.Management.DataDir\n\tengine := types.Engine(cfg.Management.Store.Engine)\n\n\ts, err := store.NewStore(ctx, engine, datadir, nil, true)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create store: %w\", err)\n\t}\n\tdefer func() {\n\t\tif err := s.Close(ctx); err != nil {\n\t\t\tlog.Debugf(\"close store: %v\", err)\n\t\t}\n\t}()\n\n\treturn fn(ctx, s)\n}","originalFile":"package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/netbirdio/netbird/formatter/hook\"\n\ttokencmd \"github.com/netbirdio/netbird/management/cmd/token\"\n\t\"github.com/netbirdio/netbird/management/server/store\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util\"\n)\n\n// newTokenCommands creates the token command tree with combined-specific store opener.\nfunc newTokenCommands() *cobra.Command {\n\treturn tokencmd.NewCommands(withTokenStore)\n}\n\n// withTokenStore loads the combined YAML config, initializes the store, and calls fn.\nfunc withTokenStore(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error {\n\tif err := util.InitLog(\"error\", \"console\"); err != nil {\n\t\treturn fmt.Errorf(\"init log: %w\", err)\n\t}\n\n\tctx := context.WithValue(cmd.Context(), hook.ExecutionContextKey, hook.SystemSource) //nolint:staticcheck\n\n\tcfg, err := LoadConfig(configPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"load config: %w\", err)\n\t}\n\n\tif dsn := cfg.Server.Store.DSN; dsn != \"\" {\n\t\tswitch strings.ToLower(cfg.Server.Store.Engine) {\n\t\tcase \"postgres\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_POSTGRES_DSN\", dsn)\n\t\tcase \"mysql\":\n\t\t\tos.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)\n\t\t}\n\t}\n\n\tdatadir := cfg.Management.DataDir\n\tengine := types.Engine(cfg.Management.Store.Engine)\n\n\ts, err := store.NewStore(ctx, engine, datadir, nil, true)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create store: %w\", err)\n\t}\n\tdefer func() {\n\t\tif err := s.Close(ctx); err != nil {\n\t\t\tlog.Debugf(\"close store: %v\", err)\n\t\t}\n\t}()\n\n\treturn fn(ctx, s)\n}\n","structuredPatch":[{"oldStart":42,"oldLines":6,"newStart":42,"newLines":9,"lines":[" os.Setenv(\"NB_STORE_ENGINE_MYSQL_DSN\", dsn)"," }"," }","+ if file := cfg.Server.Store.File; file != \"\" {","+ os.Setenv(\"NB_STORE_ENGINE_SQLITE_FILE\", file)","+ }"," "," datadir := cfg.Management.DataDir"," engine := types.Engine(cfg.Management.Store.Engine)"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"b02f5764-88db-480f-8fd9-127cad3df2fa"} +{"parentUuid":"8314730c-c5b8-45a3-8a53-36f1fcbe311a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01LLH2FUGsjq85Gg4rnogzQA","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01ENUUkGtFG12DixcAEqEfEu","name":"TaskUpdate","input":{"taskId":"3","status":"completed"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":452,"cache_read_input_tokens":84387,"output_tokens":70,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":452,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYeoJgVbYVGtaM1PBWY3P","type":"assistant","uuid":"58266d6f-e694-457e-b8f9-be09334d77a3","timestamp":"2026-03-02T19:23:27.260Z"} +{"parentUuid":"58266d6f-e694-457e-b8f9-be09334d77a3","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01ENUUkGtFG12DixcAEqEfEu","type":"tool_result","content":"Updated task #3 status"}]},"uuid":"e535dbf6-d9b6-4e65-a502-6c32b830027b","timestamp":"2026-03-02T19:23:27.341Z","toolUseResult":{"success":true,"taskId":"3","updatedFields":["status"],"statusChange":{"from":"in_progress","to":"completed"}},"sourceToolAssistantUUID":"58266d6f-e694-457e-b8f9-be09334d77a3"} +{"parentUuid":"e535dbf6-d9b6-4e65-a502-6c32b830027b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01XsE27P4c4NhwpHykHZziRQ","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01FQjKXSbwQCa6V1oyDmtpSz","name":"TaskUpdate","input":{"taskId":"4","status":"in_progress"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":88,"cache_read_input_tokens":84839,"output_tokens":72,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":88,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYeoJvsHZtpLwHTJDt6U2","type":"assistant","uuid":"92b1fc10-c85d-4e35-9203-c87f840681e3","timestamp":"2026-03-02T19:23:30.663Z"} +{"parentUuid":"92b1fc10-c85d-4e35-9203-c87f840681e3","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01FQjKXSbwQCa6V1oyDmtpSz","type":"tool_result","content":"Updated task #4 status"}]},"uuid":"1c28e768-3764-4f80-a7c9-de9eef8268e6","timestamp":"2026-03-02T19:23:30.734Z","toolUseResult":{"success":true,"taskId":"4","updatedFields":["status"],"statusChange":{"from":"pending","to":"in_progress"}},"sourceToolAssistantUUID":"92b1fc10-c85d-4e35-9203-c87f840681e3"} +{"parentUuid":"1c28e768-3764-4f80-a7c9-de9eef8268e6","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01NgM7R99BfXto5m92HdXKTE","type":"message","role":"assistant","content":[{"type":"text","text":"Now update the example config:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":90,"cache_read_input_tokens":84927,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":90},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYeoKAudeeaEy685LtiXy","type":"assistant","uuid":"36f01647-2e7e-48e4-b3f9-9c33168d6869","timestamp":"2026-03-02T19:23:33.561Z"} +{"parentUuid":"36f01647-2e7e-48e4-b3f9-9c33168d6869","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01NgM7R99BfXto5m92HdXKTE","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_015tZj173Qehka9KRVQrkpyE","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/config.yaml.example","old_string":" # Store configuration\n store:\n engine: \"sqlite\" # sqlite, postgres, or mysql\n dsn: \"\" # Connection string for postgres or mysql\n encryptionKey: \"\"\n\n # Activity events store configuration (optional, defaults to sqlite in dataDir)\n # activityStore:\n # engine: \"sqlite\" # sqlite or postgres\n # dsn: \"\" # Connection string for postgres\n\n # Auth (embedded IdP) store configuration (optional, defaults to sqlite3 in dataDir/idp.db)\n # authStore:\n # engine: \"sqlite3\" # sqlite3 or postgres\n # dsn: \"\" # Connection string for postgres (e.g., \"host=localhost port=5432 user=postgres password=postgres dbname=netbird_idp sslmode=disable\")","new_string":" # Store configuration\n store:\n engine: \"sqlite\" # sqlite, postgres, or mysql\n dsn: \"\" # Connection string for postgres or mysql\n encryptionKey: \"\"\n # file: \"\" # Custom SQLite file path (optional, defaults to {dataDir}/store.db)\n\n # Activity events store configuration (optional, defaults to sqlite in dataDir)\n # activityStore:\n # engine: \"sqlite\" # sqlite or postgres\n # dsn: \"\" # Connection string for postgres\n # file: \"\" # Custom SQLite file path (optional, defaults to {dataDir}/events.db)\n\n # Auth (embedded IdP) store configuration (optional, defaults to sqlite3 in dataDir/idp.db)\n # authStore:\n # engine: \"sqlite3\" # sqlite3 or postgres\n # dsn: \"\" # Connection string for postgres (e.g., \"host=localhost port=5432 user=postgres password=postgres dbname=netbird_idp sslmode=disable\")\n # file: \"\" # Custom SQLite file path (optional, defaults to {dataDir}/idp.db)"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":90,"cache_read_input_tokens":84927,"output_tokens":612,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":90,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYeoKAudeeaEy685LtiXy","type":"assistant","uuid":"3bd4e06f-aa8b-4f48-ae14-7fa5d01599f5","timestamp":"2026-03-02T19:23:39.841Z"} +{"type":"file-history-snapshot","messageId":"3bd4e06f-aa8b-4f48-ae14-7fa5d01599f5","snapshot":{"messageId":"7c71f5f8-6fbe-48a5-8d9b-9322a19b9593","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v2","version":2,"backupTime":"2026-03-02T19:15:31.656Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v2","version":2,"backupTime":"2026-03-02T19:20:06.400Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v1","version":1,"backupTime":"2026-03-02T19:21:36.151Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v1","version":1,"backupTime":"2026-03-02T19:22:27.975Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v1","version":1,"backupTime":"2026-03-02T19:23:23.859Z"},"combined/config.yaml.example":{"backupFileName":"ab8acefcb43dda84@v1","version":1,"backupTime":"2026-03-02T19:24:07.613Z"}},"timestamp":"2026-03-02T19:20:06.399Z"},"isSnapshotUpdate":true} +{"parentUuid":"3bd4e06f-aa8b-4f48-ae14-7fa5d01599f5","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_015tZj173Qehka9KRVQrkpyE","toolUseID":"toolu_015tZj173Qehka9KRVQrkpyE","timestamp":"2026-03-02T19:24:07.624Z","uuid":"7a9e5143-c09b-4336-ae22-49cc9fb825aa"} +{"parentUuid":"3bd4e06f-aa8b-4f48-ae14-7fa5d01599f5","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_015tZj173Qehka9KRVQrkpyE","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/combined/config.yaml.example has been updated successfully."}]},"uuid":"42f969f0-c4a6-4c05-b254-f25a8f3c629e","timestamp":"2026-03-02T19:24:07.622Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/config.yaml.example","oldString":"# NetBird Combined Server Configuration\n# Copy this file to config.yaml and customize for your deployment\n#\n# This is a Management server with optional embedded Signal, Relay, and STUN services.\n# By default, all services run locally. You can use external services instead by\n# setting the corresponding override fields.\n#\n# Architecture:\n# - Management: Always runs locally (this IS the management server)\n# - Signal: Local by default; set 'signalUri' to use external (disables local)\n# - Relay: Local by default; set 'relays' to use external (disables local)\n# - STUN: Local on port 3478 by default; set 'stuns' to use external instead\n\nserver:\n # Main HTTP/gRPC port for all services (Management, Signal, Relay)\n listenAddress: \":443\"\n\n # Public address that peers will use to connect to this server\n # Used for relay connections and management DNS domain\n # Format: protocol://hostname:port (e.g., https://server.mycompany.com:443)\n exposedAddress: \"https://server.mycompany.com:443\"\n\n # STUN server ports (defaults to [3478] if not specified; set 'stuns' to use external)\n # stunPorts:\n # - 3478\n\n # Metrics endpoint port\n metricsPort: 9090\n\n # Healthcheck endpoint address\n healthcheckAddress: \":9000\"\n\n # Logging configuration\n logLevel: \"info\" # Default log level for all components: panic, fatal, error, warn, info, debug, trace\n logFile: \"console\" # \"console\" or path to log file\n\n # TLS configuration (optional)\n tls:\n certFile: \"\"\n keyFile: \"\"\n letsencrypt:\n enabled: false\n dataDir: \"\"\n domains: []\n email: \"\"\n awsRoute53: false\n\n # Shared secret for relay authentication (required when running local relay)\n authSecret: \"your-secret-key-here\"\n\n # Data directory for all services\n dataDir: \"/var/lib/netbird/\"\n\n # ============================================================================\n # External Service Overrides (optional)\n # Use these to point to external Signal, Relay, or STUN servers instead of\n # running them locally. When set, the corresponding local service is disabled.\n # ============================================================================\n\n # External STUN servers - disables local STUN server\n # stuns:\n # - uri: \"stun:stun.example.com:3478\"\n # - uri: \"stun:stun.example.com:3479\"\n\n # External relay servers - disables local relay server\n # relays:\n # addresses:\n # - \"rels://relay.example.com:443\"\n # credentialsTTL: \"12h\"\n # secret: \"relay-shared-secret\"\n\n # External signal server - disables local signal server\n # signalUri: \"https://signal.example.com:443\"\n\n # ============================================================================\n # Management Settings\n # ============================================================================\n\n # Metrics and updates\n disableAnonymousMetrics: false\n disableGeoliteUpdate: false\n\n # Embedded authentication/identity provider (Dex) configuration (always enabled)\n auth:\n # OIDC issuer URL - must be publicly accessible\n issuer: \"https://example.com/oauth2\"\n localAuthDisabled: false\n signKeyRefreshEnabled: false\n # OAuth2 redirect URIs for dashboard\n dashboardRedirectURIs:\n - \"https://app.example.com/nb-auth\"\n - \"https://app.example.com/nb-silent-auth\"\n # OAuth2 redirect URIs for CLI\n cliRedirectURIs:\n - \"http://localhost:53000/\"\n # Optional initial admin user\n # owner:\n # email: \"admin@example.com\"\n # password: \"initial-password\"\n\n # Store configuration\n store:\n engine: \"sqlite\" # sqlite, postgres, or mysql\n dsn: \"\" # Connection string for postgres or mysql\n encryptionKey: \"\"\n\n # Activity events store configuration (optional, defaults to sqlite in dataDir)\n # activityStore:\n # engine: \"sqlite\" # sqlite or postgres\n # dsn: \"\" # Connection string for postgres\n\n # Auth (embedded IdP) store configuration (optional, defaults to sqlite3 in dataDir/idp.db)\n # authStore:\n # engine: \"sqlite3\" # sqlite3 or postgres\n # dsn: \"\" # Connection string for postgres (e.g., \"host=localhost port=5432 user=postgres password=postgres dbname=netbird_idp sslmode=disable\")\n\n # Reverse proxy settings (optional)\n # reverseProxy:\n # trustedHTTPProxies: []\n # trustedHTTPProxiesCount: 0\n # trustedPeers: []","newString":"# NetBird Combined Server Configuration\n# Copy this file to config.yaml and customize for your deployment\n#\n# This is a Management server with optional embedded Signal, Relay, and STUN services.\n# By default, all services run locally. You can use external services instead by\n# setting the corresponding override fields.\n#\n# Architecture:\n# - Management: Always runs locally (this IS the management server)\n# - Signal: Local by default; set 'signalUri' to use external (disables local)\n# - Relay: Local by default; set 'relays' to use external (disables local)\n# - STUN: Local on port 3478 by default; set 'stuns' to use external instead\n\nserver:\n # Main HTTP/gRPC port for all services (Management, Signal, Relay)\n listenAddress: \":443\"\n\n # Public address that peers will use to connect to this server\n # Used for relay connections and management DNS domain\n # Format: protocol://hostname:port (e.g., https://server.mycompany.com:443)\n exposedAddress: \"https://server.mycompany.com:443\"\n\n # STUN server ports (defaults to [3478] if not specified; set 'stuns' to use external)\n # stunPorts:\n # - 3478\n\n # Metrics endpoint port\n metricsPort: 9090\n\n # Healthcheck endpoint address\n healthcheckAddress: \":9000\"\n\n # Logging configuration\n logLevel: \"info\" # Default log level for all components: panic, fatal, error, warn, info, debug, trace\n logFile: \"console\" # \"console\" or path to log file\n\n # TLS configuration (optional)\n tls:\n certFile: \"\"\n keyFile: \"\"\n letsencrypt:\n enabled: false\n dataDir: \"\"\n domains: []\n email: \"\"\n awsRoute53: false\n\n # Shared secret for relay authentication (required when running local relay)\n authSecret: \"your-secret-key-here\"\n\n # Data directory for all services\n dataDir: \"/var/lib/netbird/\"\n\n # ============================================================================\n # External Service Overrides (optional)\n # Use these to point to external Signal, Relay, or STUN servers instead of\n # running them locally. When set, the corresponding local service is disabled.\n # ============================================================================\n\n # External STUN servers - disables local STUN server\n # stuns:\n # - uri: \"stun:stun.example.com:3478\"\n # - uri: \"stun:stun.example.com:3479\"\n\n # External relay servers - disables local relay server\n # relays:\n # addresses:\n # - \"rels://relay.example.com:443\"\n # credentialsTTL: \"12h\"\n # secret: \"relay-shared-secret\"\n\n # External signal server - disables local signal server\n # signalUri: \"https://signal.example.com:443\"\n\n # ============================================================================\n # Management Settings\n # ============================================================================\n\n # Metrics and updates\n disableAnonymousMetrics: false\n disableGeoliteUpdate: false\n\n # Embedded authentication/identity provider (Dex) configuration (always enabled)\n auth:\n # OIDC issuer URL - must be publicly accessible\n issuer: \"https://example.com/oauth2\"\n localAuthDisabled: false\n signKeyRefreshEnabled: false\n # OAuth2 redirect URIs for dashboard\n dashboardRedirectURIs:\n - \"https://app.example.com/nb-auth\"\n - \"https://app.example.com/nb-silent-auth\"\n # OAuth2 redirect URIs for CLI\n cliRedirectURIs:\n - \"http://localhost:53000/\"\n # Optional initial admin user\n # owner:\n # email: \"admin@example.com\"\n # password: \"initial-password\"\n\n # Store configuration\n store:\n engine: \"sqlite\" # sqlite, postgres, or mysql\n dsn: \"\" # Connection string for postgres or mysql\n encryptionKey: \"\"\n # file: \"\" # Custom SQLite file path (optional, defaults to {dataDir}/store.db)\n\n # Activity events store configuration (optional, defaults to sqlite in dataDir)\n # activityStore:\n # engine: \"sqlite\" # sqlite or postgres\n # dsn: \"\" # Connection string for postgres\n # file: \"\" # Custom SQLite file path (optional, defaults to {dataDir}/events.db)\n\n # Auth (embedded IdP) store configuration (optional, defaults to sqlite3 in dataDir/idp.db)\n # authStore:\n # engine: \"sqlite3\" # sqlite3 or postgres\n # dsn: \"\" # Connection string for postgres (e.g., \"host=localhost port=5432 user=postgres password=postgres dbname=netbird_idp sslmode=disable\")\n # file: \"\" # Custom SQLite file path (optional, defaults to {dataDir}/idp.db)\n\n # Reverse proxy settings (optional)\n # reverseProxy:\n # trustedHTTPProxies: []\n # trustedHTTPProxiesCount: 0\n # trustedPeers: []","originalFile":"# NetBird Combined Server Configuration\n# Copy this file to config.yaml and customize for your deployment\n#\n# This is a Management server with optional embedded Signal, Relay, and STUN services.\n# By default, all services run locally. You can use external services instead by\n# setting the corresponding override fields.\n#\n# Architecture:\n# - Management: Always runs locally (this IS the management server)\n# - Signal: Local by default; set 'signalUri' to use external (disables local)\n# - Relay: Local by default; set 'relays' to use external (disables local)\n# - STUN: Local on port 3478 by default; set 'stuns' to use external instead\n\nserver:\n # Main HTTP/gRPC port for all services (Management, Signal, Relay)\n listenAddress: \":443\"\n\n # Public address that peers will use to connect to this server\n # Used for relay connections and management DNS domain\n # Format: protocol://hostname:port (e.g., https://server.mycompany.com:443)\n exposedAddress: \"https://server.mycompany.com:443\"\n\n # STUN server ports (defaults to [3478] if not specified; set 'stuns' to use external)\n # stunPorts:\n # - 3478\n\n # Metrics endpoint port\n metricsPort: 9090\n\n # Healthcheck endpoint address\n healthcheckAddress: \":9000\"\n\n # Logging configuration\n logLevel: \"info\" # Default log level for all components: panic, fatal, error, warn, info, debug, trace\n logFile: \"console\" # \"console\" or path to log file\n\n # TLS configuration (optional)\n tls:\n certFile: \"\"\n keyFile: \"\"\n letsencrypt:\n enabled: false\n dataDir: \"\"\n domains: []\n email: \"\"\n awsRoute53: false\n\n # Shared secret for relay authentication (required when running local relay)\n authSecret: \"your-secret-key-here\"\n\n # Data directory for all services\n dataDir: \"/var/lib/netbird/\"\n\n # ============================================================================\n # External Service Overrides (optional)\n # Use these to point to external Signal, Relay, or STUN servers instead of\n # running them locally. When set, the corresponding local service is disabled.\n # ============================================================================\n\n # External STUN servers - disables local STUN server\n # stuns:\n # - uri: \"stun:stun.example.com:3478\"\n # - uri: \"stun:stun.example.com:3479\"\n\n # External relay servers - disables local relay server\n # relays:\n # addresses:\n # - \"rels://relay.example.com:443\"\n # credentialsTTL: \"12h\"\n # secret: \"relay-shared-secret\"\n\n # External signal server - disables local signal server\n # signalUri: \"https://signal.example.com:443\"\n\n # ============================================================================\n # Management Settings\n # ============================================================================\n\n # Metrics and updates\n disableAnonymousMetrics: false\n disableGeoliteUpdate: false\n\n # Embedded authentication/identity provider (Dex) configuration (always enabled)\n auth:\n # OIDC issuer URL - must be publicly accessible\n issuer: \"https://example.com/oauth2\"\n localAuthDisabled: false\n signKeyRefreshEnabled: false\n # OAuth2 redirect URIs for dashboard\n dashboardRedirectURIs:\n - \"https://app.example.com/nb-auth\"\n - \"https://app.example.com/nb-silent-auth\"\n # OAuth2 redirect URIs for CLI\n cliRedirectURIs:\n - \"http://localhost:53000/\"\n # Optional initial admin user\n # owner:\n # email: \"admin@example.com\"\n # password: \"initial-password\"\n\n # Store configuration\n store:\n engine: \"sqlite\" # sqlite, postgres, or mysql\n dsn: \"\" # Connection string for postgres or mysql\n encryptionKey: \"\"\n\n # Activity events store configuration (optional, defaults to sqlite in dataDir)\n # activityStore:\n # engine: \"sqlite\" # sqlite or postgres\n # dsn: \"\" # Connection string for postgres\n\n # Auth (embedded IdP) store configuration (optional, defaults to sqlite3 in dataDir/idp.db)\n # authStore:\n # engine: \"sqlite3\" # sqlite3 or postgres\n # dsn: \"\" # Connection string for postgres (e.g., \"host=localhost port=5432 user=postgres password=postgres dbname=netbird_idp sslmode=disable\")\n\n # Reverse proxy settings (optional)\n # reverseProxy:\n # trustedHTTPProxies: []\n # trustedHTTPProxiesCount: 0\n # trustedPeers: []\n","structuredPatch":[{"oldStart":103,"oldLines":16,"newStart":103,"newLines":19,"lines":[" engine: \"sqlite\" # sqlite, postgres, or mysql"," dsn: \"\" # Connection string for postgres or mysql"," encryptionKey: \"\"","+ # file: \"\" # Custom SQLite file path (optional, defaults to {dataDir}/store.db)"," "," # Activity events store configuration (optional, defaults to sqlite in dataDir)"," # activityStore:"," # engine: \"sqlite\" # sqlite or postgres"," # dsn: \"\" # Connection string for postgres","+ # file: \"\" # Custom SQLite file path (optional, defaults to {dataDir}/events.db)"," "," # Auth (embedded IdP) store configuration (optional, defaults to sqlite3 in dataDir/idp.db)"," # authStore:"," # engine: \"sqlite3\" # sqlite3 or postgres"," # dsn: \"\" # Connection string for postgres (e.g., \"host=localhost port=5432 user=postgres password=postgres dbname=netbird_idp sslmode=disable\")","+ # file: \"\" # Custom SQLite file path (optional, defaults to {dataDir}/idp.db)"," "," # Reverse proxy settings (optional)"," # reverseProxy:"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"3bd4e06f-aa8b-4f48-ae14-7fa5d01599f5"} +{"parentUuid":"42f969f0-c4a6-4c05-b254-f25a8f3c629e","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01Mao1FELikdwjBRi9mev9Z1","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01JCpuarvP3BNR9AQSzpMDzg","name":"TaskUpdate","input":{"taskId":"4","status":"completed"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":656,"cache_read_input_tokens":85017,"output_tokens":70,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":656,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYeoMudD23Zcg8a6Cu4wg","type":"assistant","uuid":"e4e1830d-16d2-4f2d-b698-411b2b0712b7","timestamp":"2026-03-02T19:24:11.157Z"} +{"parentUuid":"e4e1830d-16d2-4f2d-b698-411b2b0712b7","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01JCpuarvP3BNR9AQSzpMDzg","type":"tool_result","content":"Updated task #4 status"}]},"uuid":"9ec92d21-981d-44f9-9533-f662e4009c1a","timestamp":"2026-03-02T19:24:11.213Z","toolUseResult":{"success":true,"taskId":"4","updatedFields":["status"],"statusChange":{"from":"in_progress","to":"completed"}},"sourceToolAssistantUUID":"e4e1830d-16d2-4f2d-b698-411b2b0712b7"} +{"parentUuid":"9ec92d21-981d-44f9-9533-f662e4009c1a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01AbYMx9yvetJgxYhCyQ2dVg","type":"message","role":"assistant","content":[{"type":"text","text":"Now let me verify everything compiles:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":88,"cache_read_input_tokens":85673,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":88},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYeoNAA5Q13cG3e9578RP","type":"assistant","uuid":"c4853aa3-9413-4538-b5bd-34273dec61b8","timestamp":"2026-03-02T19:24:14.504Z"} +{"parentUuid":"c4853aa3-9413-4538-b5bd-34273dec61b8","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01AbYMx9yvetJgxYhCyQ2dVg","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01WAcB6trXzj9cCZ41JgpBkK","name":"Bash","input":{"command":"go build ./combined/... 2>&1","description":"Build combined package","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":88,"cache_read_input_tokens":85673,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":88},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYeoNAA5Q13cG3e9578RP","type":"assistant","uuid":"3ae4fdce-0b22-46fe-9465-f1eae951c137","timestamp":"2026-03-02T19:24:15.498Z"} +{"parentUuid":"3ae4fdce-0b22-46fe-9465-f1eae951c137","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01AbYMx9yvetJgxYhCyQ2dVg","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01GJ2jUbv7U18iY13wxN26G5","name":"Bash","input":{"command":"go build ./management/server/store/... 2>&1","description":"Build store package","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":88,"cache_read_input_tokens":85673,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":88},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYeoNAA5Q13cG3e9578RP","type":"assistant","uuid":"2b209641-4706-4a9d-a0ae-bafee670ce59","timestamp":"2026-03-02T19:24:16.386Z"} +{"parentUuid":"2b209641-4706-4a9d-a0ae-bafee670ce59","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01AbYMx9yvetJgxYhCyQ2dVg","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01KirH611nMHN8C8tjbpW3WM","name":"Bash","input":{"command":"go build ./management/server/activity/store/... 2>&1","description":"Build activity store package","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":88,"cache_read_input_tokens":85673,"output_tokens":340,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":88,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYeoNAA5Q13cG3e9578RP","type":"assistant","uuid":"895e9ebe-9276-4548-968c-f218bacb2738","timestamp":"2026-03-02T19:24:16.970Z"} +{"parentUuid":"895e9ebe-9276-4548-968c-f218bacb2738","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":3,"totalLines":0,"totalBytes":0,"taskId":"byt9u7vqb","timeoutMs":120000},"toolUseID":"bash-progress-0","parentToolUseID":"toolu_01WAcB6trXzj9cCZ41JgpBkK","uuid":"69c24c91-5806-413e-8961-c3c289038137","timestamp":"2026-03-02T19:24:18.704Z"} +{"parentUuid":"69c24c91-5806-413e-8961-c3c289038137","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":4,"totalLines":0,"totalBytes":0,"taskId":"byt9u7vqb","timeoutMs":120000},"toolUseID":"bash-progress-1","parentToolUseID":"toolu_01WAcB6trXzj9cCZ41JgpBkK","uuid":"8017927c-c836-40b5-9c72-ccd7030e5bde","timestamp":"2026-03-02T19:24:19.704Z"} +{"parentUuid":"8017927c-c836-40b5-9c72-ccd7030e5bde","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":5,"totalLines":0,"totalBytes":0,"taskId":"byt9u7vqb","timeoutMs":120000},"toolUseID":"bash-progress-2","parentToolUseID":"toolu_01WAcB6trXzj9cCZ41JgpBkK","uuid":"5b17eb12-726f-4de1-af18-180dd0393752","timestamp":"2026-03-02T19:24:20.706Z"} +{"parentUuid":"5b17eb12-726f-4de1-af18-180dd0393752","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":6,"totalLines":0,"totalBytes":0,"taskId":"byt9u7vqb","timeoutMs":120000},"toolUseID":"bash-progress-3","parentToolUseID":"toolu_01WAcB6trXzj9cCZ41JgpBkK","uuid":"4dbefb56-5a1c-45d0-b903-aa346266635d","timestamp":"2026-03-02T19:24:21.707Z"} +{"parentUuid":"4dbefb56-5a1c-45d0-b903-aa346266635d","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":7,"totalLines":0,"totalBytes":0,"taskId":"byt9u7vqb","timeoutMs":120000},"toolUseID":"bash-progress-4","parentToolUseID":"toolu_01WAcB6trXzj9cCZ41JgpBkK","uuid":"3631e324-593d-450c-93af-d92c4b8350d5","timestamp":"2026-03-02T19:24:22.712Z"} +{"parentUuid":"3631e324-593d-450c-93af-d92c4b8350d5","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":8,"totalLines":0,"totalBytes":0,"taskId":"byt9u7vqb","timeoutMs":120000},"toolUseID":"bash-progress-5","parentToolUseID":"toolu_01WAcB6trXzj9cCZ41JgpBkK","uuid":"ad217f10-eed8-4780-ad16-c85917d2cab7","timestamp":"2026-03-02T19:24:23.714Z"} +{"parentUuid":"ad217f10-eed8-4780-ad16-c85917d2cab7","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":9,"totalLines":0,"totalBytes":0,"taskId":"byt9u7vqb","timeoutMs":120000},"toolUseID":"bash-progress-6","parentToolUseID":"toolu_01WAcB6trXzj9cCZ41JgpBkK","uuid":"3df22118-2d64-4ac2-a67e-0e493b88115f","timestamp":"2026-03-02T19:24:24.716Z"} +{"parentUuid":"3df22118-2d64-4ac2-a67e-0e493b88115f","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":10,"totalLines":0,"totalBytes":0,"taskId":"byt9u7vqb","timeoutMs":120000},"toolUseID":"bash-progress-7","parentToolUseID":"toolu_01WAcB6trXzj9cCZ41JgpBkK","uuid":"93771308-1b1c-4b96-a3e3-061c0a96bc7f","timestamp":"2026-03-02T19:24:25.721Z"} +{"parentUuid":"93771308-1b1c-4b96-a3e3-061c0a96bc7f","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":11,"totalLines":0,"totalBytes":0,"taskId":"byt9u7vqb","timeoutMs":120000},"toolUseID":"bash-progress-8","parentToolUseID":"toolu_01WAcB6trXzj9cCZ41JgpBkK","uuid":"43b11a76-581a-40d1-b7ac-6fdde0303ad2","timestamp":"2026-03-02T19:24:26.722Z"} +{"parentUuid":"43b11a76-581a-40d1-b7ac-6fdde0303ad2","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":12,"totalLines":0,"totalBytes":0,"taskId":"byt9u7vqb","timeoutMs":120000},"toolUseID":"bash-progress-9","parentToolUseID":"toolu_01WAcB6trXzj9cCZ41JgpBkK","uuid":"5ce3cc50-5b6c-4dc1-9ca6-f4656c1bf83c","timestamp":"2026-03-02T19:24:27.728Z"} +{"parentUuid":"5ce3cc50-5b6c-4dc1-9ca6-f4656c1bf83c","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":13,"totalLines":0,"totalBytes":0,"taskId":"byt9u7vqb","timeoutMs":120000},"toolUseID":"bash-progress-10","parentToolUseID":"toolu_01WAcB6trXzj9cCZ41JgpBkK","uuid":"9a6e1883-492d-42d1-ba31-70870650f24a","timestamp":"2026-03-02T19:24:28.729Z"} +{"parentUuid":"9a6e1883-492d-42d1-ba31-70870650f24a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":14,"totalLines":0,"totalBytes":0,"taskId":"byt9u7vqb","timeoutMs":120000},"toolUseID":"bash-progress-11","parentToolUseID":"toolu_01WAcB6trXzj9cCZ41JgpBkK","uuid":"2c659ca7-e4a9-4593-ad60-b591c3bec6e6","timestamp":"2026-03-02T19:24:29.733Z"} +{"parentUuid":"2c659ca7-e4a9-4593-ad60-b591c3bec6e6","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":15,"totalLines":0,"totalBytes":0,"taskId":"byt9u7vqb","timeoutMs":120000},"toolUseID":"bash-progress-12","parentToolUseID":"toolu_01WAcB6trXzj9cCZ41JgpBkK","uuid":"92bddc29-f35a-4342-ad04-65c722c4853d","timestamp":"2026-03-02T19:24:30.732Z"} +{"parentUuid":"92bddc29-f35a-4342-ad04-65c722c4853d","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":16,"totalLines":0,"totalBytes":0,"taskId":"byt9u7vqb","timeoutMs":120000},"toolUseID":"bash-progress-13","parentToolUseID":"toolu_01WAcB6trXzj9cCZ41JgpBkK","uuid":"4943470c-c46b-42b1-b582-41314eda4f6b","timestamp":"2026-03-02T19:24:31.733Z"} +{"parentUuid":"3ae4fdce-0b22-46fe-9465-f1eae951c137","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01WAcB6trXzj9cCZ41JgpBkK","type":"tool_result","content":"","is_error":false}]},"uuid":"85695af0-d9bf-47f2-a9df-9a1939a3f597","timestamp":"2026-03-02T19:24:32.445Z","toolUseResult":{"stdout":"","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"3ae4fdce-0b22-46fe-9465-f1eae951c137"} +{"parentUuid":"2b209641-4706-4a9d-a0ae-bafee670ce59","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01GJ2jUbv7U18iY13wxN26G5","type":"tool_result","content":"","is_error":false}]},"uuid":"ffe5428b-859c-4b1b-89f2-637246aa639c","timestamp":"2026-03-02T19:24:32.901Z","toolUseResult":{"stdout":"","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"2b209641-4706-4a9d-a0ae-bafee670ce59"} +{"parentUuid":"895e9ebe-9276-4548-968c-f218bacb2738","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01KirH611nMHN8C8tjbpW3WM","type":"tool_result","content":"","is_error":false}]},"uuid":"fac753c8-3348-41ef-bdde-afd320dbd6a2","timestamp":"2026-03-02T19:24:33.266Z","toolUseResult":{"stdout":"","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"895e9ebe-9276-4548-968c-f218bacb2738"} +{"parentUuid":"fac753c8-3348-41ef-bdde-afd320dbd6a2","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01Fiz65AR6Hz1Kif1QHXLaVZ","type":"message","role":"assistant","content":[{"type":"text","text":"All three packages compile cleanly. Here's a summary of the changes:\n\n### Changes\n\n**`combined/cmd/config.go`**\n- Added `File string` field to `StoreConfig` — applies to `store`, `activityStore`, and `authStore`\n- Updated `buildEmbeddedIdPConfig()` to use `authStore.File` when set instead of the default `{dataDir}/idp.db`\n\n**`combined/cmd/root.go`**\n- In `initializeConfig()`, sets `NB_STORE_ENGINE_SQLITE_FILE` env var when `store.file` is configured\n- Sets `NB_ACTIVITY_EVENT_SQLITE_FILE` env var when `activityStore.file` is configured\n\n**`combined/cmd/token.go`**\n- Sets `NB_STORE_ENGINE_SQLITE_FILE` env var when `store.file` is configured\n\n**`management/server/store/sql_store.go`**\n- `NewSqliteStore()` checks `NB_STORE_ENGINE_SQLITE_FILE` env var for custom file path\n- Properly separates file path from `?` query params before `filepath.IsAbs`/`filepath.Join`, then re-attaches them\n- Falls back to `?cache=shared` on non-Windows only when no user-provided query params\n\n**`management/server/activity/store/sql_store.go`**\n- `initDatabase()` checks `NB_ACTIVITY_EVENT_SQLITE_FILE` env var for custom file path\n- Uses `filepath.IsAbs` to decide whether to join with `dataDir` or use directly\n\n**`combined/config.yaml.example`**\n- Added `file` field documentation to all three store sections"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":383,"cache_read_input_tokens":85761,"output_tokens":413,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":383,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYeoPnXspD43BWnL8bRJ7","type":"assistant","uuid":"be2449a1-ea28-42aa-a95a-6be5493e128b","timestamp":"2026-03-02T19:24:44.106Z"} +{"parentUuid":"be2449a1-ea28-42aa-a95a-6be5493e128b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"Stop","hookName":"Stop","command":"entire hooks claude-code stop"},"parentToolUseID":"0be30fa7-56cd-4496-865f-4f27ba079d69","toolUseID":"0be30fa7-56cd-4496-865f-4f27ba079d69","timestamp":"2026-03-02T19:24:44.113Z","uuid":"63a39a58-14b4-4c14-9287-b04f9955d939"} +{"parentUuid":"63a39a58-14b4-4c14-9287-b04f9955d939","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"system","subtype":"stop_hook_summary","hookCount":1,"hookInfos":[{"command":"entire hooks claude-code stop","durationMs":2829}],"hookErrors":[],"preventedContinuation":false,"stopReason":"","hasOutput":true,"level":"suggestion","timestamp":"2026-03-02T19:24:46.943Z","uuid":"ce96b10d-6541-4e0d-93a5-592d2f4d7251","toolUseID":"0be30fa7-56cd-4496-865f-4f27ba079d69"} +{"parentUuid":"ce96b10d-6541-4e0d-93a5-592d2f4d7251","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"system","subtype":"turn_duration","durationMs":151544,"timestamp":"2026-03-02T19:24:46.944Z","uuid":"2865e313-14e8-487a-b05f-30a7fbd87225","isMeta":false} +{"type":"file-history-snapshot","messageId":"fceae473-f86c-4e8b-aeb7-8efb687f75af","snapshot":{"messageId":"fceae473-f86c-4e8b-aeb7-8efb687f75af","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v2","version":2,"backupTime":"2026-03-02T19:15:31.656Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v2","version":2,"backupTime":"2026-03-02T19:20:06.400Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v2","version":2,"backupTime":"2026-03-02T19:45:28.129Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/config.yaml.example":{"backupFileName":"ab8acefcb43dda84@v2","version":2,"backupTime":"2026-03-02T19:45:28.132Z"}},"timestamp":"2026-03-02T19:45:28.128Z"},"isSnapshotUpdate":false} +{"parentUuid":"2865e313-14e8-487a-b05f-30a7fbd87225","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":"summarize the changes in a short pr description"},"uuid":"fceae473-f86c-4e8b-aeb7-8efb687f75af","timestamp":"2026-03-02T19:45:26.875Z","permissionMode":"default"} +{"parentUuid":"fceae473-f86c-4e8b-aeb7-8efb687f75af","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01QjdTzikCWeeiS3jRV7gT91","type":"message","role":"assistant","content":[{"type":"text","text":"\n\n**Add custom SQLite file path support for combined server stores**\n\nAllows specifying custom SQLite file paths via `store.file`, `activityStore.file`, and `authStore.file` in the combined server config. Without this, all SQLite databases are hardcoded to `{dataDir}/store.db`, `{dataDir}/events.db`, and `{dataDir}/idp.db` respectively.\n\nThe combined server propagates the file paths to the underlying store packages via `NB_STORE_ENGINE_SQLITE_FILE` and `NB_ACTIVITY_EVENT_SQLITE_FILE` env vars (matching the existing DSN env var pattern). The auth store file is passed directly through the embedded IdP config.\n\nBoth absolute paths and SQLite URI query parameters (e.g., `?cache=shared`) are supported."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":71,"cache_read_input_tokens":86555,"output_tokens":190,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":71,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYepzGJnKNKt7DgfTh83P","type":"assistant","uuid":"b587741d-07ae-4c03-b9ab-e104ed8914cc","timestamp":"2026-03-02T19:45:35.828Z"} +{"parentUuid":"b587741d-07ae-4c03-b9ab-e104ed8914cc","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"Stop","hookName":"Stop","command":"entire hooks claude-code stop"},"parentToolUseID":"9bb4d43b-189b-4065-8671-1ec4dd73cef6","toolUseID":"9bb4d43b-189b-4065-8671-1ec4dd73cef6","timestamp":"2026-03-02T19:45:35.867Z","uuid":"cb409bd7-25f9-49dd-bb9c-08320c3b7cc6"} +{"parentUuid":"cb409bd7-25f9-49dd-bb9c-08320c3b7cc6","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"system","subtype":"stop_hook_summary","hookCount":1,"hookInfos":[{"command":"entire hooks claude-code stop","durationMs":428}],"hookErrors":[],"preventedContinuation":false,"stopReason":"","hasOutput":true,"level":"suggestion","timestamp":"2026-03-02T19:45:36.297Z","uuid":"6b892db9-6bca-4bbe-bd05-ae012ec5dc32","toolUseID":"9bb4d43b-189b-4065-8671-1ec4dd73cef6"} +{"type":"file-history-snapshot","messageId":"135dc014-9882-4788-9d9a-e69610bdeecc","snapshot":{"messageId":"135dc014-9882-4788-9d9a-e69610bdeecc","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v2","version":2,"backupTime":"2026-03-02T19:15:31.656Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v2","version":2,"backupTime":"2026-03-02T19:20:06.400Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v2","version":2,"backupTime":"2026-03-02T19:45:28.129Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/config.yaml.example":{"backupFileName":"ab8acefcb43dda84@v2","version":2,"backupTime":"2026-03-02T19:45:28.132Z"}},"timestamp":"2026-03-02T19:55:23.659Z"},"isSnapshotUpdate":false} +{"parentUuid":"6b892db9-6bca-4bbe-bd05-ae012ec5dc32","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":"Verify each finding against the current code and only fix it if needed.\n\nIn `@combined/cmd/config.go` around lines 572 - 574, The assignment of\nc.Server.AuthStore.File to authStorageFile should resolve relative paths against\nmgmt.DataDir so the auth DB lives under the management data directory like the\nother sqlite stores; update the logic where authStorageFile is set (referencing\nc.Server.AuthStore.File and authStorageFile) to check if the value is non-empty\nand not an absolute path (use filepath.IsAbs) and if so join it with\nmgmt.DataDir (use filepath.Join and optionally filepath.Clean) before assigning;\nkeep absolute paths unchanged."},"uuid":"135dc014-9882-4788-9d9a-e69610bdeecc","timestamp":"2026-03-02T19:55:22.444Z","permissionMode":"default"} +{"parentUuid":"135dc014-9882-4788-9d9a-e69610bdeecc","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_015wzgtTMmVvcMuR9nR5QUaV","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01B7Fksi3fcifPRUZd1ZsWgc","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go","offset":560,"limit":25},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":169,"cache_read_input_tokens":86817,"output_tokens":113,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":169,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYeqkBmuFAtB4A6cHoMVq","type":"assistant","uuid":"f14b5ad6-00e9-41d2-bb4c-7bfde1f6a6c2","timestamp":"2026-03-02T19:55:27.788Z"} +{"parentUuid":"f14b5ad6-00e9-41d2-bb4c-7bfde1f6a6c2","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01B7Fksi3fcifPRUZd1ZsWgc","toolUseID":"toolu_01B7Fksi3fcifPRUZd1ZsWgc","timestamp":"2026-03-02T19:55:27.851Z","uuid":"55bd48f2-e0b2-4db6-bbb0-a220e4388cdd"} +{"parentUuid":"f14b5ad6-00e9-41d2-bb4c-7bfde1f6a6c2","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01B7Fksi3fcifPRUZd1ZsWgc","type":"tool_result","content":" 560→\t\tauthStorageType = c.Server.AuthStore.Engine\n 561→\t}\n 562→\tif authStorageType == \"\" {\n 563→\t\tauthStorageType = \"sqlite3\"\n 564→\t}\n 565→\tauthStorageFile := \"\"\n 566→\tif authStorageType == \"postgres\" {\n 567→\t\tif authStorageDSN == \"\" {\n 568→\t\t\treturn nil, fmt.Errorf(\"authStore.dsn is required when authStore.engine is postgres\")\n 569→\t\t}\n 570→\t} else {\n 571→\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n 572→\t\tif c.Server.AuthStore.File != \"\" {\n 573→\t\t\tauthStorageFile = c.Server.AuthStore.File\n 574→\t\t}\n 575→\t}\n 576→\n 577→\tcfg := &idp.EmbeddedIdPConfig{\n 578→\t\tEnabled: true,\n 579→\t\tIssuer: mgmt.Auth.Issuer,\n 580→\t\tLocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,\n 581→\t\tSignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,\n 582→\t\tStorage: idp.EmbeddedStorageConfig{\n 583→\t\t\tType: authStorageType,\n 584→\t\t\tConfig: idp.EmbeddedStorageTypeConfig{"}]},"uuid":"f5653bda-e3b0-4b50-8416-92e58b71071b","timestamp":"2026-03-02T19:55:27.850Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go","content":"\t\tauthStorageType = c.Server.AuthStore.Engine\n\t}\n\tif authStorageType == \"\" {\n\t\tauthStorageType = \"sqlite3\"\n\t}\n\tauthStorageFile := \"\"\n\tif authStorageType == \"postgres\" {\n\t\tif authStorageDSN == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"authStore.dsn is required when authStore.engine is postgres\")\n\t\t}\n\t} else {\n\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n\t\tif c.Server.AuthStore.File != \"\" {\n\t\t\tauthStorageFile = c.Server.AuthStore.File\n\t\t}\n\t}\n\n\tcfg := &idp.EmbeddedIdPConfig{\n\t\tEnabled: true,\n\t\tIssuer: mgmt.Auth.Issuer,\n\t\tLocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,\n\t\tSignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,\n\t\tStorage: idp.EmbeddedStorageConfig{\n\t\t\tType: authStorageType,\n\t\t\tConfig: idp.EmbeddedStorageTypeConfig{","numLines":25,"startLine":560,"totalLines":762}},"sourceToolAssistantUUID":"f14b5ad6-00e9-41d2-bb4c-7bfde1f6a6c2"} +{"parentUuid":"f5653bda-e3b0-4b50-8416-92e58b71071b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_012PC6bBuLoBu9fFFHB828Yr","type":"message","role":"assistant","content":[{"type":"text","text":"Yes, this needs fixing. A relative path like `\"custom_idp.db\"` would be used as-is without being resolved against `mgmt.DataDir`, unlike how the other stores handle it."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":485,"cache_read_input_tokens":86986,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":485},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYeqkqyJz6c1g8147RXkH","type":"assistant","uuid":"fd7ce337-a083-428b-aed1-2ae298d71bfc","timestamp":"2026-03-02T19:55:36.996Z"} +{"parentUuid":"fd7ce337-a083-428b-aed1-2ae298d71bfc","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_012PC6bBuLoBu9fFFHB828Yr","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01MNcXwqC2LnXVFK31KrpPZ7","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go","limit":22},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":485,"cache_read_input_tokens":86986,"output_tokens":141,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":485,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYeqkqyJz6c1g8147RXkH","type":"assistant","uuid":"d62d1ea6-3c63-4a95-a7bd-197aa838462b","timestamp":"2026-03-02T19:55:37.450Z"} +{"parentUuid":"d62d1ea6-3c63-4a95-a7bd-197aa838462b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01MNcXwqC2LnXVFK31KrpPZ7","toolUseID":"toolu_01MNcXwqC2LnXVFK31KrpPZ7","timestamp":"2026-03-02T19:55:37.539Z","uuid":"6b05c1da-f3ed-491e-ba95-b2e120134f79"} +{"parentUuid":"d62d1ea6-3c63-4a95-a7bd-197aa838462b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01MNcXwqC2LnXVFK31KrpPZ7","type":"tool_result","content":" 1→package cmd\n 2→\n 3→import (\n 4→\t\"context\"\n 5→\t\"fmt\"\n 6→\t\"net\"\n 7→\t\"net/netip\"\n 8→\t\"os\"\n 9→\t\"path\"\n 10→\t\"strings\"\n 11→\t\"time\"\n 12→\n 13→\tlog \"github.com/sirupsen/logrus\"\n 14→\t\"gopkg.in/yaml.v3\"\n 15→\n 16→\t\"github.com/netbirdio/netbird/management/server/idp\"\n 17→\t\"github.com/netbirdio/netbird/management/server/types\"\n 18→\t\"github.com/netbirdio/netbird/util\"\n 19→\t\"github.com/netbirdio/netbird/util/crypt\"\n 20→\n 21→\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n 22→)"}]},"uuid":"ddf6d949-8528-4cd7-9d70-a132048487f6","timestamp":"2026-03-02T19:55:37.538Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go","content":"package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/netbirdio/netbird/management/server/idp\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n\n\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n)","numLines":22,"startLine":1,"totalLines":762}},"sourceToolAssistantUUID":"d62d1ea6-3c63-4a95-a7bd-197aa838462b"} +{"parentUuid":"ddf6d949-8528-4cd7-9d70-a132048487f6","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01X1GXezwyJuDLrryABNuzok","type":"message","role":"assistant","content":[{"type":"text","text":"Need to add `path/filepath` import since this file currently only uses `path`:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":406,"cache_read_input_tokens":87471,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":406},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYeqmZnkx8VNf8E9uQYce","type":"assistant","uuid":"1144c49c-3f5c-4937-b0f0-bdf704894a05","timestamp":"2026-03-02T19:55:47.496Z"} +{"parentUuid":"1144c49c-3f5c-4937-b0f0-bdf704894a05","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01X1GXezwyJuDLrryABNuzok","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_013b7rzNygPzNLuTh4sr5BqW","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go","old_string":"\t\"path\"","new_string":"\t\"path\"\n\t\"path/filepath\""},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":406,"cache_read_input_tokens":87471,"output_tokens":148,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":406,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYeqmZnkx8VNf8E9uQYce","type":"assistant","uuid":"e763c195-a5d3-4a66-9db7-6c641b26a84c","timestamp":"2026-03-02T19:55:48.234Z"} +{"parentUuid":"e763c195-a5d3-4a66-9db7-6c641b26a84c","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_013b7rzNygPzNLuTh4sr5BqW","toolUseID":"toolu_013b7rzNygPzNLuTh4sr5BqW","timestamp":"2026-03-03T08:54:17.085Z","uuid":"f1f7c32d-aee6-45cf-a551-f34381b9e473"} +{"parentUuid":"e763c195-a5d3-4a66-9db7-6c641b26a84c","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_013b7rzNygPzNLuTh4sr5BqW","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go has been updated successfully."}]},"uuid":"fc19682f-e0c0-4353-8045-b1edb0e4cf37","timestamp":"2026-03-03T08:54:17.081Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go","oldString":"package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/netbirdio/netbird/management/server/idp\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n\n\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n)\n\n// CombinedConfig is the root configuration for the combined server.\n// The combined server is primarily a Management server with optional embedded\n// Signal, Relay, and STUN services.\n//\n// Architecture:\n// - Management: Always runs locally (this IS the management server)\n// - Signal: Runs locally by default; disabled if server.signalUri is set\n// - Relay: Runs locally by default; disabled if server.relays is set\n// - STUN: Runs locally on port 3478 by default; disabled if server.stuns is set\n//\n// All user-facing settings are under \"server\". The relay/signal/management\n// fields are internal and populated automatically from server settings.\ntype CombinedConfig struct {\n\tServer ServerConfig `yaml:\"server\"`\n\n\t// Internal configs - populated from Server settings, not user-configurable\n\tRelay RelayConfig `yaml:\"-\"`\n\tSignal SignalConfig `yaml:\"-\"`\n\tManagement ManagementConfig `yaml:\"-\"`\n}\n\n// ServerConfig contains server-wide settings\n// In simplified mode, this contains all configuration\ntype ServerConfig struct {\n\tListenAddress string `yaml:\"listenAddress\"`\n\tMetricsPort int `yaml:\"metricsPort\"`\n\tHealthcheckAddress string `yaml:\"healthcheckAddress\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tLogFile string `yaml:\"logFile\"`\n\tTLS TLSConfig `yaml:\"tls\"`\n\n\t// Simplified config fields (used when relay/signal/management sections are omitted)\n\tExposedAddress string `yaml:\"exposedAddress\"` // Public address with protocol (e.g., \"https://example.com:443\")\n\tStunPorts []int `yaml:\"stunPorts\"` // STUN ports (empty to disable local STUN)\n\tAuthSecret string `yaml:\"authSecret\"` // Shared secret for relay authentication\n\tDataDir string `yaml:\"dataDir\"` // Data directory for all services\n\n\t// External service overrides (simplified mode)\n\t// When these are set, the corresponding local service is NOT started\n\t// and these values are used for client configuration instead\n\tStuns []HostConfig `yaml:\"stuns\"` // External STUN servers (disables local STUN)\n\tRelays RelaysConfig `yaml:\"relays\"` // External relay servers (disables local relay)\n\tSignalURI string `yaml:\"signalUri\"` // External signal server (disables local signal)\n\n\t// Management settings (simplified mode)\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tActivityStore StoreConfig `yaml:\"activityStore\"`\n\tAuthStore StoreConfig `yaml:\"authStore\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// TLSConfig contains TLS/HTTPS settings\ntype TLSConfig struct {\n\tCertFile string `yaml:\"certFile\"`\n\tKeyFile string `yaml:\"keyFile\"`\n\tLetsEncrypt LetsEncryptConfig `yaml:\"letsencrypt\"`\n}\n\n// LetsEncryptConfig contains Let's Encrypt settings\ntype LetsEncryptConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDomains []string `yaml:\"domains\"`\n\tEmail string `yaml:\"email\"`\n\tAWSRoute53 bool `yaml:\"awsRoute53\"`\n}\n\n// RelayConfig contains relay service settings\ntype RelayConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tExposedAddress string `yaml:\"exposedAddress\"`\n\tAuthSecret string `yaml:\"authSecret\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tStun StunConfig `yaml:\"stun\"`\n}\n\n// StunConfig contains embedded STUN service settings\ntype StunConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tPorts []int `yaml:\"ports\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// SignalConfig contains signal service settings\ntype SignalConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// ManagementConfig contains management service settings\ntype ManagementConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDnsDomain string `yaml:\"dnsDomain\"`\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tDisableDefaultPolicy bool `yaml:\"disableDefaultPolicy\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStuns []HostConfig `yaml:\"stuns\"`\n\tRelays RelaysConfig `yaml:\"relays\"`\n\tSignalURI string `yaml:\"signalUri\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// AuthConfig contains authentication/identity provider settings\ntype AuthConfig struct {\n\tIssuer string `yaml:\"issuer\"`\n\tLocalAuthDisabled bool `yaml:\"localAuthDisabled\"`\n\tSignKeyRefreshEnabled bool `yaml:\"signKeyRefreshEnabled\"`\n\tStorage AuthStorageConfig `yaml:\"storage\"`\n\tDashboardRedirectURIs []string `yaml:\"dashboardRedirectURIs\"`\n\tCLIRedirectURIs []string `yaml:\"cliRedirectURIs\"`\n\tOwner *AuthOwnerConfig `yaml:\"owner,omitempty\"`\n}\n\n// AuthStorageConfig contains auth storage settings\ntype AuthStorageConfig struct {\n\tType string `yaml:\"type\"`\n\tFile string `yaml:\"file\"`\n}\n\n// AuthOwnerConfig contains initial admin user settings\ntype AuthOwnerConfig struct {\n\tEmail string `yaml:\"email\"`\n\tPassword string `yaml:\"password\"`\n}\n\n// HostConfig represents a STUN/TURN/Signal host\ntype HostConfig struct {\n\tURI string `yaml:\"uri\"`\n\tProto string `yaml:\"proto,omitempty\"` // udp, dtls, tcp, http, https - defaults based on URI scheme\n\tUsername string `yaml:\"username,omitempty\"`\n\tPassword string `yaml:\"password,omitempty\"`\n}\n\n// RelaysConfig contains external relay server settings for clients\ntype RelaysConfig struct {\n\tAddresses []string `yaml:\"addresses\"`\n\tCredentialsTTL string `yaml:\"credentialsTTL\"`\n\tSecret string `yaml:\"secret\"`\n}\n\n// StoreConfig contains database settings\ntype StoreConfig struct {\n\tEngine string `yaml:\"engine\"`\n\tEncryptionKey string `yaml:\"encryptionKey\"`\n\tDSN string `yaml:\"dsn\"` // Connection string for postgres or mysql engines\n\tFile string `yaml:\"file\"` // SQLite database file path (optional, defaults to dataDir)\n}\n\n// ReverseProxyConfig contains reverse proxy settings\ntype ReverseProxyConfig struct {\n\tTrustedHTTPProxies []string `yaml:\"trustedHTTPProxies\"`\n\tTrustedHTTPProxiesCount uint `yaml:\"trustedHTTPProxiesCount\"`\n\tTrustedPeers []string `yaml:\"trustedPeers\"`\n}\n\n// DefaultConfig returns a CombinedConfig with default values\nfunc DefaultConfig() *CombinedConfig {\n\treturn &CombinedConfig{\n\t\tServer: ServerConfig{\n\t\t\tListenAddress: \":443\",\n\t\t\tMetricsPort: 9090,\n\t\t\tHealthcheckAddress: \":9000\",\n\t\t\tLogLevel: \"info\",\n\t\t\tLogFile: \"console\",\n\t\t\tStunPorts: []int{3478},\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t\tRelay: RelayConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\tStun: StunConfig{\n\t\t\t\tEnabled: false,\n\t\t\t\tPorts: []int{3478},\n\t\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\t},\n\t\t},\n\t\tSignal: SignalConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t},\n\t\tManagement: ManagementConfig{\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRelays: RelaysConfig{\n\t\t\t\tCredentialsTTL: \"12h\",\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t}\n}\n\n// hasRequiredSettings returns true if the configuration has the required server settings\nfunc (c *CombinedConfig) hasRequiredSettings() bool {\n\treturn c.Server.ExposedAddress != \"\"\n}\n\n// parseExposedAddress extracts protocol, host, and host:port from the exposed address\n// Input format: \"https://example.com:443\" or \"http://example.com:8080\" or \"example.com:443\"\n// Returns: protocol (\"https\" or \"http\"), hostname only, and host:port\nfunc parseExposedAddress(exposedAddress string) (protocol, hostname, hostPort string) {\n\t// Default to https if no protocol specified\n\tprotocol = \"https\"\n\thostPort = exposedAddress\n\n\t// Check for protocol prefix\n\tif strings.HasPrefix(exposedAddress, \"https://\") {\n\t\tprotocol = \"https\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"https://\")\n\t} else if strings.HasPrefix(exposedAddress, \"http://\") {\n\t\tprotocol = \"http\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"http://\")\n\t}\n\n\t// Extract hostname (without port)\n\thostname = hostPort\n\tif host, _, err := net.SplitHostPort(hostPort); err == nil {\n\t\thostname = host\n\t}\n\n\treturn protocol, hostname, hostPort\n}\n\n// ApplySimplifiedDefaults populates internal relay/signal/management configs from server settings.\n// Management is always enabled. Signal, Relay, and STUN are enabled unless external\n// overrides are configured (server.signalUri, server.relays, server.stuns).\nfunc (c *CombinedConfig) ApplySimplifiedDefaults() {\n\tif !c.hasRequiredSettings() {\n\t\treturn\n\t}\n\n\t// Parse exposed address to extract protocol and hostname\n\texposedProto, exposedHost, exposedHostPort := parseExposedAddress(c.Server.ExposedAddress)\n\n\t// Check for external service overrides\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\thasExternalSignal := c.Server.SignalURI != \"\"\n\thasExternalStuns := len(c.Server.Stuns) > 0\n\n\t// Default stunPorts to [3478] if not specified and no external STUN\n\tif len(c.Server.StunPorts) == 0 && !hasExternalStuns {\n\t\tc.Server.StunPorts = []int{3478}\n\t}\n\n\tc.applyRelayDefaults(exposedProto, exposedHostPort, hasExternalRelay, hasExternalStuns)\n\tc.applySignalDefaults(hasExternalSignal)\n\tc.applyManagementDefaults(exposedHost)\n\n\t// Auto-configure client settings (stuns, relays, signalUri)\n\tc.autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort, hasExternalStuns, hasExternalRelay, hasExternalSignal)\n}\n\n// applyRelayDefaults configures the relay service if no external relay is configured.\nfunc (c *CombinedConfig) applyRelayDefaults(exposedProto, exposedHostPort string, hasExternalRelay, hasExternalStuns bool) {\n\tif hasExternalRelay {\n\t\treturn\n\t}\n\n\tc.Relay.Enabled = true\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\tc.Relay.ExposedAddress = fmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort)\n\tc.Relay.AuthSecret = c.Server.AuthSecret\n\tif c.Relay.LogLevel == \"\" {\n\t\tc.Relay.LogLevel = c.Server.LogLevel\n\t}\n\n\t// Enable local STUN only if no external STUN servers and stunPorts are configured\n\tif !hasExternalStuns && len(c.Server.StunPorts) > 0 {\n\t\tc.Relay.Stun.Enabled = true\n\t\tc.Relay.Stun.Ports = c.Server.StunPorts\n\t\tif c.Relay.Stun.LogLevel == \"\" {\n\t\t\tc.Relay.Stun.LogLevel = c.Server.LogLevel\n\t\t}\n\t}\n}\n\n// applySignalDefaults configures the signal service if no external signal is configured.\nfunc (c *CombinedConfig) applySignalDefaults(hasExternalSignal bool) {\n\tif hasExternalSignal {\n\t\treturn\n\t}\n\n\tc.Signal.Enabled = true\n\tif c.Signal.LogLevel == \"\" {\n\t\tc.Signal.LogLevel = c.Server.LogLevel\n\t}\n}\n\n// applyManagementDefaults configures the management service (always enabled).\nfunc (c *CombinedConfig) applyManagementDefaults(exposedHost string) {\n\tc.Management.Enabled = true\n\tif c.Management.LogLevel == \"\" {\n\t\tc.Management.LogLevel = c.Server.LogLevel\n\t}\n\tif c.Management.DataDir == \"\" || c.Management.DataDir == \"/var/lib/netbird/\" {\n\t\tc.Management.DataDir = c.Server.DataDir\n\t}\n\tc.Management.DnsDomain = exposedHost\n\tc.Management.DisableAnonymousMetrics = c.Server.DisableAnonymousMetrics\n\tc.Management.DisableGeoliteUpdate = c.Server.DisableGeoliteUpdate\n\t// Copy auth config from server if management auth issuer is not set\n\tif c.Management.Auth.Issuer == \"\" && c.Server.Auth.Issuer != \"\" {\n\t\tc.Management.Auth = c.Server.Auth\n\t}\n\n\t// Copy store config from server if not set\n\tif c.Management.Store.Engine == \"\" || c.Management.Store.Engine == \"sqlite\" {\n\t\tif c.Server.Store.Engine != \"\" {\n\t\t\tc.Management.Store = c.Server.Store\n\t\t}\n\t}\n\n\t// Copy reverse proxy config from server\n\tif len(c.Server.ReverseProxy.TrustedHTTPProxies) > 0 || c.Server.ReverseProxy.TrustedHTTPProxiesCount > 0 || len(c.Server.ReverseProxy.TrustedPeers) > 0 {\n\t\tc.Management.ReverseProxy = c.Server.ReverseProxy\n\t}\n}\n\n// autoConfigureClientSettings sets up STUN/relay/signal URIs for clients\n// External overrides from server config take precedence over auto-generated values\nfunc (c *CombinedConfig) autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort string, hasExternalStuns, hasExternalRelay, hasExternalSignal bool) {\n\t// Determine relay protocol from exposed protocol\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\n\t// Configure STUN servers for clients\n\tif hasExternalStuns {\n\t\t// Use external STUN servers from server config\n\t\tc.Management.Stuns = c.Server.Stuns\n\t} else if len(c.Server.StunPorts) > 0 && len(c.Management.Stuns) == 0 {\n\t\t// Auto-configure local STUN servers for all ports\n\t\tfor _, port := range c.Server.StunPorts {\n\t\t\tc.Management.Stuns = append(c.Management.Stuns, HostConfig{\n\t\t\t\tURI: fmt.Sprintf(\"stun:%s:%d\", exposedHost, port),\n\t\t\t})\n\t\t}\n\t}\n\n\t// Configure relay for clients\n\tif hasExternalRelay {\n\t\t// Use external relay config from server\n\t\tc.Management.Relays = c.Server.Relays\n\t} else if len(c.Management.Relays.Addresses) == 0 {\n\t\t// Auto-configure local relay\n\t\tc.Management.Relays.Addresses = []string{\n\t\t\tfmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort),\n\t\t}\n\t}\n\tif c.Management.Relays.Secret == \"\" {\n\t\tc.Management.Relays.Secret = c.Server.AuthSecret\n\t}\n\tif c.Management.Relays.CredentialsTTL == \"\" {\n\t\tc.Management.Relays.CredentialsTTL = \"12h\"\n\t}\n\n\t// Configure signal for clients\n\tif hasExternalSignal {\n\t\t// Use external signal URI from server config\n\t\tc.Management.SignalURI = c.Server.SignalURI\n\t} else if c.Management.SignalURI == \"\" {\n\t\t// Auto-configure local signal\n\t\tc.Management.SignalURI = fmt.Sprintf(\"%s://%s\", exposedProto, exposedHostPort)\n\t}\n}\n\n// LoadConfig loads configuration from a YAML file\nfunc LoadConfig(configPath string) (*CombinedConfig, error) {\n\tcfg := DefaultConfig()\n\n\tif configPath == \"\" {\n\t\treturn cfg, nil\n\t}\n\n\tdata, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read config file: %w\", err)\n\t}\n\n\tif err := yaml.Unmarshal(data, cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse config file: %w\", err)\n\t}\n\n\t// Populate internal configs from server settings\n\tcfg.ApplySimplifiedDefaults()\n\n\treturn cfg, nil\n}\n\n// Validate validates the configuration\nfunc (c *CombinedConfig) Validate() error {\n\tif c.Server.ExposedAddress == \"\" {\n\t\treturn fmt.Errorf(\"server.exposedAddress is required\")\n\t}\n\tif c.Server.DataDir == \"\" {\n\t\treturn fmt.Errorf(\"server.dataDir is required\")\n\t}\n\n\t// Validate STUN ports\n\tseen := make(map[int]bool)\n\tfor _, port := range c.Server.StunPorts {\n\t\tif port <= 0 || port > 65535 {\n\t\t\treturn fmt.Errorf(\"invalid server.stunPorts value %d: must be between 1 and 65535\", port)\n\t\t}\n\t\tif seen[port] {\n\t\t\treturn fmt.Errorf(\"duplicate STUN port %d in server.stunPorts\", port)\n\t\t}\n\t\tseen[port] = true\n\t}\n\n\t// authSecret is required only if running local relay (no external relay configured)\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\tif !hasExternalRelay && c.Server.AuthSecret == \"\" {\n\t\treturn fmt.Errorf(\"server.authSecret is required when running local relay\")\n\t}\n\n\treturn nil\n}\n\n// HasTLSCert returns true if TLS certificate files are configured\nfunc (c *CombinedConfig) HasTLSCert() bool {\n\treturn c.Server.TLS.CertFile != \"\" && c.Server.TLS.KeyFile != \"\"\n}\n\n// HasLetsEncrypt returns true if Let's Encrypt is configured\nfunc (c *CombinedConfig) HasLetsEncrypt() bool {\n\treturn c.Server.TLS.LetsEncrypt.Enabled &&\n\t\tc.Server.TLS.LetsEncrypt.DataDir != \"\" &&\n\t\tlen(c.Server.TLS.LetsEncrypt.Domains) > 0\n}\n\n// parseExplicitProtocol parses an explicit protocol string to nbconfig.Protocol\nfunc parseExplicitProtocol(proto string) (nbconfig.Protocol, bool) {\n\tswitch strings.ToLower(proto) {\n\tcase \"udp\":\n\t\treturn nbconfig.UDP, true\n\tcase \"dtls\":\n\t\treturn nbconfig.DTLS, true\n\tcase \"tcp\":\n\t\treturn nbconfig.TCP, true\n\tcase \"http\":\n\t\treturn nbconfig.HTTP, true\n\tcase \"https\":\n\t\treturn nbconfig.HTTPS, true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\n// parseStunProtocol determines protocol for STUN/TURN servers.\n// stun: → UDP, stuns: → DTLS, turn: → UDP, turns: → DTLS\n// Explicit proto overrides URI scheme. Defaults to UDP.\nfunc parseStunProtocol(uri, proto string) nbconfig.Protocol {\n\tif proto != \"\" {\n\t\tif p, ok := parseExplicitProtocol(proto); ok {\n\t\t\treturn p\n\t\t}\n\t}\n\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"stuns:\"):\n\t\treturn nbconfig.DTLS\n\tcase strings.HasPrefix(uri, \"turns:\"):\n\t\treturn nbconfig.DTLS\n\tdefault:\n\t\t// stun:, turn:, or no scheme - default to UDP\n\t\treturn nbconfig.UDP\n\t}\n}\n\n// parseSignalProtocol determines protocol for Signal servers.\n// https:// → HTTPS, http:// → HTTP. Defaults to HTTPS.\nfunc parseSignalProtocol(uri string) nbconfig.Protocol {\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"http://\"):\n\t\treturn nbconfig.HTTP\n\tdefault:\n\t\t// https:// or no scheme - default to HTTPS\n\t\treturn nbconfig.HTTPS\n\t}\n}\n\n// stripSignalProtocol removes the protocol prefix from a signal URI.\n// Returns just the host:port (e.g., \"selfhosted2.demo.netbird.io:443\").\nfunc stripSignalProtocol(uri string) string {\n\turi = strings.TrimPrefix(uri, \"https://\")\n\turi = strings.TrimPrefix(uri, \"http://\")\n\treturn uri\n}\n\nfunc buildRelayConfig(relays RelaysConfig) (*nbconfig.Relay, error) {\n\tvar ttl time.Duration\n\tif relays.CredentialsTTL != \"\" {\n\t\tvar err error\n\t\tttl, err = time.ParseDuration(relays.CredentialsTTL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid relay credentials TTL %q: %w\", relays.CredentialsTTL, err)\n\t\t}\n\t}\n\treturn &nbconfig.Relay{\n\t\tAddresses: relays.Addresses,\n\t\tCredentialsTTL: util.Duration{Duration: ttl},\n\t\tSecret: relays.Secret,\n\t}, nil\n}\n\n// buildEmbeddedIdPConfig builds the embedded IdP configuration.\n// authStore overrides auth.storage when set.\nfunc (c *CombinedConfig) buildEmbeddedIdPConfig(mgmt ManagementConfig) (*idp.EmbeddedIdPConfig, error) {\n\tauthStorageType := mgmt.Auth.Storage.Type\n\tauthStorageDSN := c.Server.AuthStore.DSN\n\tif c.Server.AuthStore.Engine != \"\" {\n\t\tauthStorageType = c.Server.AuthStore.Engine\n\t}\n\tif authStorageType == \"\" {\n\t\tauthStorageType = \"sqlite3\"\n\t}\n\tauthStorageFile := \"\"\n\tif authStorageType == \"postgres\" {\n\t\tif authStorageDSN == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"authStore.dsn is required when authStore.engine is postgres\")\n\t\t}\n\t} else {\n\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n\t\tif c.Server.AuthStore.File != \"\" {\n\t\t\tauthStorageFile = c.Server.AuthStore.File\n\t\t}\n\t}\n\n\tcfg := &idp.EmbeddedIdPConfig{\n\t\tEnabled: true,\n\t\tIssuer: mgmt.Auth.Issuer,\n\t\tLocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,\n\t\tSignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,\n\t\tStorage: idp.EmbeddedStorageConfig{\n\t\t\tType: authStorageType,\n\t\t\tConfig: idp.EmbeddedStorageTypeConfig{\n\t\t\t\tFile: authStorageFile,\n\t\t\t\tDSN: authStorageDSN,\n\t\t\t},\n\t\t},\n\t\tDashboardRedirectURIs: mgmt.Auth.DashboardRedirectURIs,\n\t\tCLIRedirectURIs: mgmt.Auth.CLIRedirectURIs,\n\t}\n\n\tif mgmt.Auth.Owner != nil && mgmt.Auth.Owner.Email != \"\" {\n\t\tcfg.Owner = &idp.OwnerConfig{\n\t\t\tEmail: mgmt.Auth.Owner.Email,\n\t\t\tHash: mgmt.Auth.Owner.Password,\n\t\t}\n\t}\n\n\treturn cfg, nil\n}\n\n// ToManagementConfig converts CombinedConfig to management server config\nfunc (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {\n\tmgmt := c.Management\n\n\t// Build STUN hosts\n\tvar stuns []*nbconfig.Host\n\tfor _, s := range mgmt.Stuns {\n\t\tstuns = append(stuns, &nbconfig.Host{\n\t\t\tURI: s.URI,\n\t\t\tProto: parseStunProtocol(s.URI, s.Proto),\n\t\t\tUsername: s.Username,\n\t\t\tPassword: s.Password,\n\t\t})\n\t}\n\n\t// Build relay config\n\tvar relayConfig *nbconfig.Relay\n\tif len(mgmt.Relays.Addresses) > 0 || mgmt.Relays.Secret != \"\" {\n\t\trelay, err := buildRelayConfig(mgmt.Relays)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trelayConfig = relay\n\t}\n\n\t// Build signal config\n\tvar signalConfig *nbconfig.Host\n\tif mgmt.SignalURI != \"\" {\n\t\tsignalConfig = &nbconfig.Host{\n\t\t\tURI: stripSignalProtocol(mgmt.SignalURI),\n\t\t\tProto: parseSignalProtocol(mgmt.SignalURI),\n\t\t}\n\t}\n\n\t// Build store config\n\tstoreConfig := nbconfig.StoreConfig{\n\t\tEngine: types.Engine(mgmt.Store.Engine),\n\t}\n\n\t// Build reverse proxy config\n\treverseProxy := nbconfig.ReverseProxy{\n\t\tTrustedHTTPProxiesCount: mgmt.ReverseProxy.TrustedHTTPProxiesCount,\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedHTTPProxies {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedHTTPProxies = append(reverseProxy.TrustedHTTPProxies, prefix)\n\t\t}\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedPeers {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedPeers = append(reverseProxy.TrustedPeers, prefix)\n\t\t}\n\t}\n\n\t// Build HTTP config (required, even if empty)\n\thttpConfig := &nbconfig.HttpServerConfig{}\n\n\t// Build embedded IDP config (always enabled in combined server)\n\tembeddedIdP, err := c.buildEmbeddedIdPConfig(mgmt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Set HTTP config fields for embedded IDP\n\thttpConfig.AuthIssuer = mgmt.Auth.Issuer\n\thttpConfig.AuthAudience = \"netbird-dashboard\"\n\thttpConfig.AuthClientID = httpConfig.AuthAudience\n\thttpConfig.CLIAuthAudience = \"netbird-cli\"\n\thttpConfig.AuthUserIDClaim = \"sub\"\n\thttpConfig.AuthKeysLocation = mgmt.Auth.Issuer + \"/keys\"\n\thttpConfig.OIDCConfigEndpoint = mgmt.Auth.Issuer + \"/.well-known/openid-configuration\"\n\thttpConfig.IdpSignKeyRefreshEnabled = mgmt.Auth.SignKeyRefreshEnabled\n\tcallbackURL := strings.TrimSuffix(httpConfig.AuthIssuer, \"/oauth2\")\n\thttpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull\n\n\treturn &nbconfig.Config{\n\t\tStuns: stuns,\n\t\tRelay: relayConfig,\n\t\tSignal: signalConfig,\n\t\tDatadir: mgmt.DataDir,\n\t\tDataStoreEncryptionKey: mgmt.Store.EncryptionKey,\n\t\tHttpConfig: httpConfig,\n\t\tStoreConfig: storeConfig,\n\t\tReverseProxy: reverseProxy,\n\t\tDisableDefaultPolicy: mgmt.DisableDefaultPolicy,\n\t\tEmbeddedIdP: embeddedIdP,\n\t}, nil\n}\n\n// ApplyEmbeddedIdPConfig applies embedded IdP configuration to the management config.\n// This mirrors the logic in management/cmd/management.go ApplyEmbeddedIdPConfig.\nfunc ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config, mgmtPort int, disableSingleAccMode bool) error {\n\tif cfg.EmbeddedIdP == nil || !cfg.EmbeddedIdP.Enabled {\n\t\treturn nil\n\t}\n\n\t// Embedded IdP requires single account mode\n\tif disableSingleAccMode {\n\t\treturn fmt.Errorf(\"embedded IdP requires single account mode; multiple account mode is not supported with embedded IdP\")\n\t}\n\n\t// Set LocalAddress for embedded IdP, used for internal JWT validation\n\tcfg.EmbeddedIdP.LocalAddress = fmt.Sprintf(\"localhost:%d\", mgmtPort)\n\n\t// Set storage defaults based on Datadir\n\tif cfg.EmbeddedIdP.Storage.Type == \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Type = \"sqlite3\"\n\t}\n\tif cfg.EmbeddedIdP.Storage.Config.File == \"\" && cfg.Datadir != \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")\n\t}\n\n\tissuer := cfg.EmbeddedIdP.Issuer\n\n\t// Ensure HttpConfig exists\n\tif cfg.HttpConfig == nil {\n\t\tcfg.HttpConfig = &nbconfig.HttpServerConfig{}\n\t}\n\n\t// Set HttpConfig values from EmbeddedIdP\n\tcfg.HttpConfig.AuthIssuer = issuer\n\tcfg.HttpConfig.AuthAudience = \"netbird-dashboard\"\n\tcfg.HttpConfig.CLIAuthAudience = \"netbird-cli\"\n\tcfg.HttpConfig.AuthUserIDClaim = \"sub\"\n\tcfg.HttpConfig.AuthKeysLocation = issuer + \"/keys\"\n\tcfg.HttpConfig.OIDCConfigEndpoint = issuer + \"/.well-known/openid-configuration\"\n\tcfg.HttpConfig.IdpSignKeyRefreshEnabled = true\n\n\treturn nil\n}\n\n// EnsureEncryptionKey generates an encryption key if not set.\n// Unlike management server, we don't write back to the config file.\nfunc EnsureEncryptionKey(ctx context.Context, cfg *nbconfig.Config) error {\n\tif cfg.DataStoreEncryptionKey != \"\" {\n\t\treturn nil\n\t}\n\n\tlog.WithContext(ctx).Infof(\"DataStoreEncryptionKey is not set, generating a new key\")\n\tkey, err := crypt.GenerateKey()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate datastore encryption key: %v\", err)\n\t}\n\tcfg.DataStoreEncryptionKey = key\n\tkeyPreview := key[:8] + \"...\"\n\tlog.WithContext(ctx).Warnf(\"DataStoreEncryptionKey generated (%s); add it to your config file under 'server.store.encryptionKey' to persist across restarts\", keyPreview)\n\n\treturn nil\n}\n\n// LogConfigInfo logs informational messages about the loaded configuration\nfunc LogConfigInfo(cfg *nbconfig.Config) {\n\tif cfg.EmbeddedIdP != nil && cfg.EmbeddedIdP.Enabled {\n\t\tlog.Infof(\"running with the embedded IdP: %v\", cfg.EmbeddedIdP.Issuer)\n\t}\n\tif cfg.Relay != nil {\n\t\tlog.Infof(\"Relay addresses: %v\", cfg.Relay.Addresses)\n\t}\n}","newString":"package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/netbirdio/netbird/management/server/idp\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n\n\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n)\n\n// CombinedConfig is the root configuration for the combined server.\n// The combined server is primarily a Management server with optional embedded\n// Signal, Relay, and STUN services.\n//\n// Architecture:\n// - Management: Always runs locally (this IS the management server)\n// - Signal: Runs locally by default; disabled if server.signalUri is set\n// - Relay: Runs locally by default; disabled if server.relays is set\n// - STUN: Runs locally on port 3478 by default; disabled if server.stuns is set\n//\n// All user-facing settings are under \"server\". The relay/signal/management\n// fields are internal and populated automatically from server settings.\ntype CombinedConfig struct {\n\tServer ServerConfig `yaml:\"server\"`\n\n\t// Internal configs - populated from Server settings, not user-configurable\n\tRelay RelayConfig `yaml:\"-\"`\n\tSignal SignalConfig `yaml:\"-\"`\n\tManagement ManagementConfig `yaml:\"-\"`\n}\n\n// ServerConfig contains server-wide settings\n// In simplified mode, this contains all configuration\ntype ServerConfig struct {\n\tListenAddress string `yaml:\"listenAddress\"`\n\tMetricsPort int `yaml:\"metricsPort\"`\n\tHealthcheckAddress string `yaml:\"healthcheckAddress\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tLogFile string `yaml:\"logFile\"`\n\tTLS TLSConfig `yaml:\"tls\"`\n\n\t// Simplified config fields (used when relay/signal/management sections are omitted)\n\tExposedAddress string `yaml:\"exposedAddress\"` // Public address with protocol (e.g., \"https://example.com:443\")\n\tStunPorts []int `yaml:\"stunPorts\"` // STUN ports (empty to disable local STUN)\n\tAuthSecret string `yaml:\"authSecret\"` // Shared secret for relay authentication\n\tDataDir string `yaml:\"dataDir\"` // Data directory for all services\n\n\t// External service overrides (simplified mode)\n\t// When these are set, the corresponding local service is NOT started\n\t// and these values are used for client configuration instead\n\tStuns []HostConfig `yaml:\"stuns\"` // External STUN servers (disables local STUN)\n\tRelays RelaysConfig `yaml:\"relays\"` // External relay servers (disables local relay)\n\tSignalURI string `yaml:\"signalUri\"` // External signal server (disables local signal)\n\n\t// Management settings (simplified mode)\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tActivityStore StoreConfig `yaml:\"activityStore\"`\n\tAuthStore StoreConfig `yaml:\"authStore\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// TLSConfig contains TLS/HTTPS settings\ntype TLSConfig struct {\n\tCertFile string `yaml:\"certFile\"`\n\tKeyFile string `yaml:\"keyFile\"`\n\tLetsEncrypt LetsEncryptConfig `yaml:\"letsencrypt\"`\n}\n\n// LetsEncryptConfig contains Let's Encrypt settings\ntype LetsEncryptConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDomains []string `yaml:\"domains\"`\n\tEmail string `yaml:\"email\"`\n\tAWSRoute53 bool `yaml:\"awsRoute53\"`\n}\n\n// RelayConfig contains relay service settings\ntype RelayConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tExposedAddress string `yaml:\"exposedAddress\"`\n\tAuthSecret string `yaml:\"authSecret\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tStun StunConfig `yaml:\"stun\"`\n}\n\n// StunConfig contains embedded STUN service settings\ntype StunConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tPorts []int `yaml:\"ports\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// SignalConfig contains signal service settings\ntype SignalConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// ManagementConfig contains management service settings\ntype ManagementConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDnsDomain string `yaml:\"dnsDomain\"`\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tDisableDefaultPolicy bool `yaml:\"disableDefaultPolicy\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStuns []HostConfig `yaml:\"stuns\"`\n\tRelays RelaysConfig `yaml:\"relays\"`\n\tSignalURI string `yaml:\"signalUri\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// AuthConfig contains authentication/identity provider settings\ntype AuthConfig struct {\n\tIssuer string `yaml:\"issuer\"`\n\tLocalAuthDisabled bool `yaml:\"localAuthDisabled\"`\n\tSignKeyRefreshEnabled bool `yaml:\"signKeyRefreshEnabled\"`\n\tStorage AuthStorageConfig `yaml:\"storage\"`\n\tDashboardRedirectURIs []string `yaml:\"dashboardRedirectURIs\"`\n\tCLIRedirectURIs []string `yaml:\"cliRedirectURIs\"`\n\tOwner *AuthOwnerConfig `yaml:\"owner,omitempty\"`\n}\n\n// AuthStorageConfig contains auth storage settings\ntype AuthStorageConfig struct {\n\tType string `yaml:\"type\"`\n\tFile string `yaml:\"file\"`\n}\n\n// AuthOwnerConfig contains initial admin user settings\ntype AuthOwnerConfig struct {\n\tEmail string `yaml:\"email\"`\n\tPassword string `yaml:\"password\"`\n}\n\n// HostConfig represents a STUN/TURN/Signal host\ntype HostConfig struct {\n\tURI string `yaml:\"uri\"`\n\tProto string `yaml:\"proto,omitempty\"` // udp, dtls, tcp, http, https - defaults based on URI scheme\n\tUsername string `yaml:\"username,omitempty\"`\n\tPassword string `yaml:\"password,omitempty\"`\n}\n\n// RelaysConfig contains external relay server settings for clients\ntype RelaysConfig struct {\n\tAddresses []string `yaml:\"addresses\"`\n\tCredentialsTTL string `yaml:\"credentialsTTL\"`\n\tSecret string `yaml:\"secret\"`\n}\n\n// StoreConfig contains database settings\ntype StoreConfig struct {\n\tEngine string `yaml:\"engine\"`\n\tEncryptionKey string `yaml:\"encryptionKey\"`\n\tDSN string `yaml:\"dsn\"` // Connection string for postgres or mysql engines\n\tFile string `yaml:\"file\"` // SQLite database file path (optional, defaults to dataDir)\n}\n\n// ReverseProxyConfig contains reverse proxy settings\ntype ReverseProxyConfig struct {\n\tTrustedHTTPProxies []string `yaml:\"trustedHTTPProxies\"`\n\tTrustedHTTPProxiesCount uint `yaml:\"trustedHTTPProxiesCount\"`\n\tTrustedPeers []string `yaml:\"trustedPeers\"`\n}\n\n// DefaultConfig returns a CombinedConfig with default values\nfunc DefaultConfig() *CombinedConfig {\n\treturn &CombinedConfig{\n\t\tServer: ServerConfig{\n\t\t\tListenAddress: \":443\",\n\t\t\tMetricsPort: 9090,\n\t\t\tHealthcheckAddress: \":9000\",\n\t\t\tLogLevel: \"info\",\n\t\t\tLogFile: \"console\",\n\t\t\tStunPorts: []int{3478},\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t\tRelay: RelayConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\tStun: StunConfig{\n\t\t\t\tEnabled: false,\n\t\t\t\tPorts: []int{3478},\n\t\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\t},\n\t\t},\n\t\tSignal: SignalConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t},\n\t\tManagement: ManagementConfig{\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRelays: RelaysConfig{\n\t\t\t\tCredentialsTTL: \"12h\",\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t}\n}\n\n// hasRequiredSettings returns true if the configuration has the required server settings\nfunc (c *CombinedConfig) hasRequiredSettings() bool {\n\treturn c.Server.ExposedAddress != \"\"\n}\n\n// parseExposedAddress extracts protocol, host, and host:port from the exposed address\n// Input format: \"https://example.com:443\" or \"http://example.com:8080\" or \"example.com:443\"\n// Returns: protocol (\"https\" or \"http\"), hostname only, and host:port\nfunc parseExposedAddress(exposedAddress string) (protocol, hostname, hostPort string) {\n\t// Default to https if no protocol specified\n\tprotocol = \"https\"\n\thostPort = exposedAddress\n\n\t// Check for protocol prefix\n\tif strings.HasPrefix(exposedAddress, \"https://\") {\n\t\tprotocol = \"https\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"https://\")\n\t} else if strings.HasPrefix(exposedAddress, \"http://\") {\n\t\tprotocol = \"http\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"http://\")\n\t}\n\n\t// Extract hostname (without port)\n\thostname = hostPort\n\tif host, _, err := net.SplitHostPort(hostPort); err == nil {\n\t\thostname = host\n\t}\n\n\treturn protocol, hostname, hostPort\n}\n\n// ApplySimplifiedDefaults populates internal relay/signal/management configs from server settings.\n// Management is always enabled. Signal, Relay, and STUN are enabled unless external\n// overrides are configured (server.signalUri, server.relays, server.stuns).\nfunc (c *CombinedConfig) ApplySimplifiedDefaults() {\n\tif !c.hasRequiredSettings() {\n\t\treturn\n\t}\n\n\t// Parse exposed address to extract protocol and hostname\n\texposedProto, exposedHost, exposedHostPort := parseExposedAddress(c.Server.ExposedAddress)\n\n\t// Check for external service overrides\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\thasExternalSignal := c.Server.SignalURI != \"\"\n\thasExternalStuns := len(c.Server.Stuns) > 0\n\n\t// Default stunPorts to [3478] if not specified and no external STUN\n\tif len(c.Server.StunPorts) == 0 && !hasExternalStuns {\n\t\tc.Server.StunPorts = []int{3478}\n\t}\n\n\tc.applyRelayDefaults(exposedProto, exposedHostPort, hasExternalRelay, hasExternalStuns)\n\tc.applySignalDefaults(hasExternalSignal)\n\tc.applyManagementDefaults(exposedHost)\n\n\t// Auto-configure client settings (stuns, relays, signalUri)\n\tc.autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort, hasExternalStuns, hasExternalRelay, hasExternalSignal)\n}\n\n// applyRelayDefaults configures the relay service if no external relay is configured.\nfunc (c *CombinedConfig) applyRelayDefaults(exposedProto, exposedHostPort string, hasExternalRelay, hasExternalStuns bool) {\n\tif hasExternalRelay {\n\t\treturn\n\t}\n\n\tc.Relay.Enabled = true\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\tc.Relay.ExposedAddress = fmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort)\n\tc.Relay.AuthSecret = c.Server.AuthSecret\n\tif c.Relay.LogLevel == \"\" {\n\t\tc.Relay.LogLevel = c.Server.LogLevel\n\t}\n\n\t// Enable local STUN only if no external STUN servers and stunPorts are configured\n\tif !hasExternalStuns && len(c.Server.StunPorts) > 0 {\n\t\tc.Relay.Stun.Enabled = true\n\t\tc.Relay.Stun.Ports = c.Server.StunPorts\n\t\tif c.Relay.Stun.LogLevel == \"\" {\n\t\t\tc.Relay.Stun.LogLevel = c.Server.LogLevel\n\t\t}\n\t}\n}\n\n// applySignalDefaults configures the signal service if no external signal is configured.\nfunc (c *CombinedConfig) applySignalDefaults(hasExternalSignal bool) {\n\tif hasExternalSignal {\n\t\treturn\n\t}\n\n\tc.Signal.Enabled = true\n\tif c.Signal.LogLevel == \"\" {\n\t\tc.Signal.LogLevel = c.Server.LogLevel\n\t}\n}\n\n// applyManagementDefaults configures the management service (always enabled).\nfunc (c *CombinedConfig) applyManagementDefaults(exposedHost string) {\n\tc.Management.Enabled = true\n\tif c.Management.LogLevel == \"\" {\n\t\tc.Management.LogLevel = c.Server.LogLevel\n\t}\n\tif c.Management.DataDir == \"\" || c.Management.DataDir == \"/var/lib/netbird/\" {\n\t\tc.Management.DataDir = c.Server.DataDir\n\t}\n\tc.Management.DnsDomain = exposedHost\n\tc.Management.DisableAnonymousMetrics = c.Server.DisableAnonymousMetrics\n\tc.Management.DisableGeoliteUpdate = c.Server.DisableGeoliteUpdate\n\t// Copy auth config from server if management auth issuer is not set\n\tif c.Management.Auth.Issuer == \"\" && c.Server.Auth.Issuer != \"\" {\n\t\tc.Management.Auth = c.Server.Auth\n\t}\n\n\t// Copy store config from server if not set\n\tif c.Management.Store.Engine == \"\" || c.Management.Store.Engine == \"sqlite\" {\n\t\tif c.Server.Store.Engine != \"\" {\n\t\t\tc.Management.Store = c.Server.Store\n\t\t}\n\t}\n\n\t// Copy reverse proxy config from server\n\tif len(c.Server.ReverseProxy.TrustedHTTPProxies) > 0 || c.Server.ReverseProxy.TrustedHTTPProxiesCount > 0 || len(c.Server.ReverseProxy.TrustedPeers) > 0 {\n\t\tc.Management.ReverseProxy = c.Server.ReverseProxy\n\t}\n}\n\n// autoConfigureClientSettings sets up STUN/relay/signal URIs for clients\n// External overrides from server config take precedence over auto-generated values\nfunc (c *CombinedConfig) autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort string, hasExternalStuns, hasExternalRelay, hasExternalSignal bool) {\n\t// Determine relay protocol from exposed protocol\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\n\t// Configure STUN servers for clients\n\tif hasExternalStuns {\n\t\t// Use external STUN servers from server config\n\t\tc.Management.Stuns = c.Server.Stuns\n\t} else if len(c.Server.StunPorts) > 0 && len(c.Management.Stuns) == 0 {\n\t\t// Auto-configure local STUN servers for all ports\n\t\tfor _, port := range c.Server.StunPorts {\n\t\t\tc.Management.Stuns = append(c.Management.Stuns, HostConfig{\n\t\t\t\tURI: fmt.Sprintf(\"stun:%s:%d\", exposedHost, port),\n\t\t\t})\n\t\t}\n\t}\n\n\t// Configure relay for clients\n\tif hasExternalRelay {\n\t\t// Use external relay config from server\n\t\tc.Management.Relays = c.Server.Relays\n\t} else if len(c.Management.Relays.Addresses) == 0 {\n\t\t// Auto-configure local relay\n\t\tc.Management.Relays.Addresses = []string{\n\t\t\tfmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort),\n\t\t}\n\t}\n\tif c.Management.Relays.Secret == \"\" {\n\t\tc.Management.Relays.Secret = c.Server.AuthSecret\n\t}\n\tif c.Management.Relays.CredentialsTTL == \"\" {\n\t\tc.Management.Relays.CredentialsTTL = \"12h\"\n\t}\n\n\t// Configure signal for clients\n\tif hasExternalSignal {\n\t\t// Use external signal URI from server config\n\t\tc.Management.SignalURI = c.Server.SignalURI\n\t} else if c.Management.SignalURI == \"\" {\n\t\t// Auto-configure local signal\n\t\tc.Management.SignalURI = fmt.Sprintf(\"%s://%s\", exposedProto, exposedHostPort)\n\t}\n}\n\n// LoadConfig loads configuration from a YAML file\nfunc LoadConfig(configPath string) (*CombinedConfig, error) {\n\tcfg := DefaultConfig()\n\n\tif configPath == \"\" {\n\t\treturn cfg, nil\n\t}\n\n\tdata, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read config file: %w\", err)\n\t}\n\n\tif err := yaml.Unmarshal(data, cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse config file: %w\", err)\n\t}\n\n\t// Populate internal configs from server settings\n\tcfg.ApplySimplifiedDefaults()\n\n\treturn cfg, nil\n}\n\n// Validate validates the configuration\nfunc (c *CombinedConfig) Validate() error {\n\tif c.Server.ExposedAddress == \"\" {\n\t\treturn fmt.Errorf(\"server.exposedAddress is required\")\n\t}\n\tif c.Server.DataDir == \"\" {\n\t\treturn fmt.Errorf(\"server.dataDir is required\")\n\t}\n\n\t// Validate STUN ports\n\tseen := make(map[int]bool)\n\tfor _, port := range c.Server.StunPorts {\n\t\tif port <= 0 || port > 65535 {\n\t\t\treturn fmt.Errorf(\"invalid server.stunPorts value %d: must be between 1 and 65535\", port)\n\t\t}\n\t\tif seen[port] {\n\t\t\treturn fmt.Errorf(\"duplicate STUN port %d in server.stunPorts\", port)\n\t\t}\n\t\tseen[port] = true\n\t}\n\n\t// authSecret is required only if running local relay (no external relay configured)\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\tif !hasExternalRelay && c.Server.AuthSecret == \"\" {\n\t\treturn fmt.Errorf(\"server.authSecret is required when running local relay\")\n\t}\n\n\treturn nil\n}\n\n// HasTLSCert returns true if TLS certificate files are configured\nfunc (c *CombinedConfig) HasTLSCert() bool {\n\treturn c.Server.TLS.CertFile != \"\" && c.Server.TLS.KeyFile != \"\"\n}\n\n// HasLetsEncrypt returns true if Let's Encrypt is configured\nfunc (c *CombinedConfig) HasLetsEncrypt() bool {\n\treturn c.Server.TLS.LetsEncrypt.Enabled &&\n\t\tc.Server.TLS.LetsEncrypt.DataDir != \"\" &&\n\t\tlen(c.Server.TLS.LetsEncrypt.Domains) > 0\n}\n\n// parseExplicitProtocol parses an explicit protocol string to nbconfig.Protocol\nfunc parseExplicitProtocol(proto string) (nbconfig.Protocol, bool) {\n\tswitch strings.ToLower(proto) {\n\tcase \"udp\":\n\t\treturn nbconfig.UDP, true\n\tcase \"dtls\":\n\t\treturn nbconfig.DTLS, true\n\tcase \"tcp\":\n\t\treturn nbconfig.TCP, true\n\tcase \"http\":\n\t\treturn nbconfig.HTTP, true\n\tcase \"https\":\n\t\treturn nbconfig.HTTPS, true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\n// parseStunProtocol determines protocol for STUN/TURN servers.\n// stun: → UDP, stuns: → DTLS, turn: → UDP, turns: → DTLS\n// Explicit proto overrides URI scheme. Defaults to UDP.\nfunc parseStunProtocol(uri, proto string) nbconfig.Protocol {\n\tif proto != \"\" {\n\t\tif p, ok := parseExplicitProtocol(proto); ok {\n\t\t\treturn p\n\t\t}\n\t}\n\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"stuns:\"):\n\t\treturn nbconfig.DTLS\n\tcase strings.HasPrefix(uri, \"turns:\"):\n\t\treturn nbconfig.DTLS\n\tdefault:\n\t\t// stun:, turn:, or no scheme - default to UDP\n\t\treturn nbconfig.UDP\n\t}\n}\n\n// parseSignalProtocol determines protocol for Signal servers.\n// https:// → HTTPS, http:// → HTTP. Defaults to HTTPS.\nfunc parseSignalProtocol(uri string) nbconfig.Protocol {\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"http://\"):\n\t\treturn nbconfig.HTTP\n\tdefault:\n\t\t// https:// or no scheme - default to HTTPS\n\t\treturn nbconfig.HTTPS\n\t}\n}\n\n// stripSignalProtocol removes the protocol prefix from a signal URI.\n// Returns just the host:port (e.g., \"selfhosted2.demo.netbird.io:443\").\nfunc stripSignalProtocol(uri string) string {\n\turi = strings.TrimPrefix(uri, \"https://\")\n\turi = strings.TrimPrefix(uri, \"http://\")\n\treturn uri\n}\n\nfunc buildRelayConfig(relays RelaysConfig) (*nbconfig.Relay, error) {\n\tvar ttl time.Duration\n\tif relays.CredentialsTTL != \"\" {\n\t\tvar err error\n\t\tttl, err = time.ParseDuration(relays.CredentialsTTL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid relay credentials TTL %q: %w\", relays.CredentialsTTL, err)\n\t\t}\n\t}\n\treturn &nbconfig.Relay{\n\t\tAddresses: relays.Addresses,\n\t\tCredentialsTTL: util.Duration{Duration: ttl},\n\t\tSecret: relays.Secret,\n\t}, nil\n}\n\n// buildEmbeddedIdPConfig builds the embedded IdP configuration.\n// authStore overrides auth.storage when set.\nfunc (c *CombinedConfig) buildEmbeddedIdPConfig(mgmt ManagementConfig) (*idp.EmbeddedIdPConfig, error) {\n\tauthStorageType := mgmt.Auth.Storage.Type\n\tauthStorageDSN := c.Server.AuthStore.DSN\n\tif c.Server.AuthStore.Engine != \"\" {\n\t\tauthStorageType = c.Server.AuthStore.Engine\n\t}\n\tif authStorageType == \"\" {\n\t\tauthStorageType = \"sqlite3\"\n\t}\n\tauthStorageFile := \"\"\n\tif authStorageType == \"postgres\" {\n\t\tif authStorageDSN == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"authStore.dsn is required when authStore.engine is postgres\")\n\t\t}\n\t} else {\n\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n\t\tif c.Server.AuthStore.File != \"\" {\n\t\t\tauthStorageFile = c.Server.AuthStore.File\n\t\t}\n\t}\n\n\tcfg := &idp.EmbeddedIdPConfig{\n\t\tEnabled: true,\n\t\tIssuer: mgmt.Auth.Issuer,\n\t\tLocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,\n\t\tSignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,\n\t\tStorage: idp.EmbeddedStorageConfig{\n\t\t\tType: authStorageType,\n\t\t\tConfig: idp.EmbeddedStorageTypeConfig{\n\t\t\t\tFile: authStorageFile,\n\t\t\t\tDSN: authStorageDSN,\n\t\t\t},\n\t\t},\n\t\tDashboardRedirectURIs: mgmt.Auth.DashboardRedirectURIs,\n\t\tCLIRedirectURIs: mgmt.Auth.CLIRedirectURIs,\n\t}\n\n\tif mgmt.Auth.Owner != nil && mgmt.Auth.Owner.Email != \"\" {\n\t\tcfg.Owner = &idp.OwnerConfig{\n\t\t\tEmail: mgmt.Auth.Owner.Email,\n\t\t\tHash: mgmt.Auth.Owner.Password,\n\t\t}\n\t}\n\n\treturn cfg, nil\n}\n\n// ToManagementConfig converts CombinedConfig to management server config\nfunc (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {\n\tmgmt := c.Management\n\n\t// Build STUN hosts\n\tvar stuns []*nbconfig.Host\n\tfor _, s := range mgmt.Stuns {\n\t\tstuns = append(stuns, &nbconfig.Host{\n\t\t\tURI: s.URI,\n\t\t\tProto: parseStunProtocol(s.URI, s.Proto),\n\t\t\tUsername: s.Username,\n\t\t\tPassword: s.Password,\n\t\t})\n\t}\n\n\t// Build relay config\n\tvar relayConfig *nbconfig.Relay\n\tif len(mgmt.Relays.Addresses) > 0 || mgmt.Relays.Secret != \"\" {\n\t\trelay, err := buildRelayConfig(mgmt.Relays)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trelayConfig = relay\n\t}\n\n\t// Build signal config\n\tvar signalConfig *nbconfig.Host\n\tif mgmt.SignalURI != \"\" {\n\t\tsignalConfig = &nbconfig.Host{\n\t\t\tURI: stripSignalProtocol(mgmt.SignalURI),\n\t\t\tProto: parseSignalProtocol(mgmt.SignalURI),\n\t\t}\n\t}\n\n\t// Build store config\n\tstoreConfig := nbconfig.StoreConfig{\n\t\tEngine: types.Engine(mgmt.Store.Engine),\n\t}\n\n\t// Build reverse proxy config\n\treverseProxy := nbconfig.ReverseProxy{\n\t\tTrustedHTTPProxiesCount: mgmt.ReverseProxy.TrustedHTTPProxiesCount,\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedHTTPProxies {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedHTTPProxies = append(reverseProxy.TrustedHTTPProxies, prefix)\n\t\t}\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedPeers {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedPeers = append(reverseProxy.TrustedPeers, prefix)\n\t\t}\n\t}\n\n\t// Build HTTP config (required, even if empty)\n\thttpConfig := &nbconfig.HttpServerConfig{}\n\n\t// Build embedded IDP config (always enabled in combined server)\n\tembeddedIdP, err := c.buildEmbeddedIdPConfig(mgmt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Set HTTP config fields for embedded IDP\n\thttpConfig.AuthIssuer = mgmt.Auth.Issuer\n\thttpConfig.AuthAudience = \"netbird-dashboard\"\n\thttpConfig.AuthClientID = httpConfig.AuthAudience\n\thttpConfig.CLIAuthAudience = \"netbird-cli\"\n\thttpConfig.AuthUserIDClaim = \"sub\"\n\thttpConfig.AuthKeysLocation = mgmt.Auth.Issuer + \"/keys\"\n\thttpConfig.OIDCConfigEndpoint = mgmt.Auth.Issuer + \"/.well-known/openid-configuration\"\n\thttpConfig.IdpSignKeyRefreshEnabled = mgmt.Auth.SignKeyRefreshEnabled\n\tcallbackURL := strings.TrimSuffix(httpConfig.AuthIssuer, \"/oauth2\")\n\thttpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull\n\n\treturn &nbconfig.Config{\n\t\tStuns: stuns,\n\t\tRelay: relayConfig,\n\t\tSignal: signalConfig,\n\t\tDatadir: mgmt.DataDir,\n\t\tDataStoreEncryptionKey: mgmt.Store.EncryptionKey,\n\t\tHttpConfig: httpConfig,\n\t\tStoreConfig: storeConfig,\n\t\tReverseProxy: reverseProxy,\n\t\tDisableDefaultPolicy: mgmt.DisableDefaultPolicy,\n\t\tEmbeddedIdP: embeddedIdP,\n\t}, nil\n}\n\n// ApplyEmbeddedIdPConfig applies embedded IdP configuration to the management config.\n// This mirrors the logic in management/cmd/management.go ApplyEmbeddedIdPConfig.\nfunc ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config, mgmtPort int, disableSingleAccMode bool) error {\n\tif cfg.EmbeddedIdP == nil || !cfg.EmbeddedIdP.Enabled {\n\t\treturn nil\n\t}\n\n\t// Embedded IdP requires single account mode\n\tif disableSingleAccMode {\n\t\treturn fmt.Errorf(\"embedded IdP requires single account mode; multiple account mode is not supported with embedded IdP\")\n\t}\n\n\t// Set LocalAddress for embedded IdP, used for internal JWT validation\n\tcfg.EmbeddedIdP.LocalAddress = fmt.Sprintf(\"localhost:%d\", mgmtPort)\n\n\t// Set storage defaults based on Datadir\n\tif cfg.EmbeddedIdP.Storage.Type == \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Type = \"sqlite3\"\n\t}\n\tif cfg.EmbeddedIdP.Storage.Config.File == \"\" && cfg.Datadir != \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")\n\t}\n\n\tissuer := cfg.EmbeddedIdP.Issuer\n\n\t// Ensure HttpConfig exists\n\tif cfg.HttpConfig == nil {\n\t\tcfg.HttpConfig = &nbconfig.HttpServerConfig{}\n\t}\n\n\t// Set HttpConfig values from EmbeddedIdP\n\tcfg.HttpConfig.AuthIssuer = issuer\n\tcfg.HttpConfig.AuthAudience = \"netbird-dashboard\"\n\tcfg.HttpConfig.CLIAuthAudience = \"netbird-cli\"\n\tcfg.HttpConfig.AuthUserIDClaim = \"sub\"\n\tcfg.HttpConfig.AuthKeysLocation = issuer + \"/keys\"\n\tcfg.HttpConfig.OIDCConfigEndpoint = issuer + \"/.well-known/openid-configuration\"\n\tcfg.HttpConfig.IdpSignKeyRefreshEnabled = true\n\n\treturn nil\n}\n\n// EnsureEncryptionKey generates an encryption key if not set.\n// Unlike management server, we don't write back to the config file.\nfunc EnsureEncryptionKey(ctx context.Context, cfg *nbconfig.Config) error {\n\tif cfg.DataStoreEncryptionKey != \"\" {\n\t\treturn nil\n\t}\n\n\tlog.WithContext(ctx).Infof(\"DataStoreEncryptionKey is not set, generating a new key\")\n\tkey, err := crypt.GenerateKey()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate datastore encryption key: %v\", err)\n\t}\n\tcfg.DataStoreEncryptionKey = key\n\tkeyPreview := key[:8] + \"...\"\n\tlog.WithContext(ctx).Warnf(\"DataStoreEncryptionKey generated (%s); add it to your config file under 'server.store.encryptionKey' to persist across restarts\", keyPreview)\n\n\treturn nil\n}\n\n// LogConfigInfo logs informational messages about the loaded configuration\nfunc LogConfigInfo(cfg *nbconfig.Config) {\n\tif cfg.EmbeddedIdP != nil && cfg.EmbeddedIdP.Enabled {\n\t\tlog.Infof(\"running with the embedded IdP: %v\", cfg.EmbeddedIdP.Issuer)\n\t}\n\tif cfg.Relay != nil {\n\t\tlog.Infof(\"Relay addresses: %v\", cfg.Relay.Addresses)\n\t}\n}","originalFile":"package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/netbirdio/netbird/management/server/idp\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n\n\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n)\n\n// CombinedConfig is the root configuration for the combined server.\n// The combined server is primarily a Management server with optional embedded\n// Signal, Relay, and STUN services.\n//\n// Architecture:\n// - Management: Always runs locally (this IS the management server)\n// - Signal: Runs locally by default; disabled if server.signalUri is set\n// - Relay: Runs locally by default; disabled if server.relays is set\n// - STUN: Runs locally on port 3478 by default; disabled if server.stuns is set\n//\n// All user-facing settings are under \"server\". The relay/signal/management\n// fields are internal and populated automatically from server settings.\ntype CombinedConfig struct {\n\tServer ServerConfig `yaml:\"server\"`\n\n\t// Internal configs - populated from Server settings, not user-configurable\n\tRelay RelayConfig `yaml:\"-\"`\n\tSignal SignalConfig `yaml:\"-\"`\n\tManagement ManagementConfig `yaml:\"-\"`\n}\n\n// ServerConfig contains server-wide settings\n// In simplified mode, this contains all configuration\ntype ServerConfig struct {\n\tListenAddress string `yaml:\"listenAddress\"`\n\tMetricsPort int `yaml:\"metricsPort\"`\n\tHealthcheckAddress string `yaml:\"healthcheckAddress\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tLogFile string `yaml:\"logFile\"`\n\tTLS TLSConfig `yaml:\"tls\"`\n\n\t// Simplified config fields (used when relay/signal/management sections are omitted)\n\tExposedAddress string `yaml:\"exposedAddress\"` // Public address with protocol (e.g., \"https://example.com:443\")\n\tStunPorts []int `yaml:\"stunPorts\"` // STUN ports (empty to disable local STUN)\n\tAuthSecret string `yaml:\"authSecret\"` // Shared secret for relay authentication\n\tDataDir string `yaml:\"dataDir\"` // Data directory for all services\n\n\t// External service overrides (simplified mode)\n\t// When these are set, the corresponding local service is NOT started\n\t// and these values are used for client configuration instead\n\tStuns []HostConfig `yaml:\"stuns\"` // External STUN servers (disables local STUN)\n\tRelays RelaysConfig `yaml:\"relays\"` // External relay servers (disables local relay)\n\tSignalURI string `yaml:\"signalUri\"` // External signal server (disables local signal)\n\n\t// Management settings (simplified mode)\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tActivityStore StoreConfig `yaml:\"activityStore\"`\n\tAuthStore StoreConfig `yaml:\"authStore\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// TLSConfig contains TLS/HTTPS settings\ntype TLSConfig struct {\n\tCertFile string `yaml:\"certFile\"`\n\tKeyFile string `yaml:\"keyFile\"`\n\tLetsEncrypt LetsEncryptConfig `yaml:\"letsencrypt\"`\n}\n\n// LetsEncryptConfig contains Let's Encrypt settings\ntype LetsEncryptConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDomains []string `yaml:\"domains\"`\n\tEmail string `yaml:\"email\"`\n\tAWSRoute53 bool `yaml:\"awsRoute53\"`\n}\n\n// RelayConfig contains relay service settings\ntype RelayConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tExposedAddress string `yaml:\"exposedAddress\"`\n\tAuthSecret string `yaml:\"authSecret\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tStun StunConfig `yaml:\"stun\"`\n}\n\n// StunConfig contains embedded STUN service settings\ntype StunConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tPorts []int `yaml:\"ports\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// SignalConfig contains signal service settings\ntype SignalConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// ManagementConfig contains management service settings\ntype ManagementConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDnsDomain string `yaml:\"dnsDomain\"`\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tDisableDefaultPolicy bool `yaml:\"disableDefaultPolicy\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStuns []HostConfig `yaml:\"stuns\"`\n\tRelays RelaysConfig `yaml:\"relays\"`\n\tSignalURI string `yaml:\"signalUri\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// AuthConfig contains authentication/identity provider settings\ntype AuthConfig struct {\n\tIssuer string `yaml:\"issuer\"`\n\tLocalAuthDisabled bool `yaml:\"localAuthDisabled\"`\n\tSignKeyRefreshEnabled bool `yaml:\"signKeyRefreshEnabled\"`\n\tStorage AuthStorageConfig `yaml:\"storage\"`\n\tDashboardRedirectURIs []string `yaml:\"dashboardRedirectURIs\"`\n\tCLIRedirectURIs []string `yaml:\"cliRedirectURIs\"`\n\tOwner *AuthOwnerConfig `yaml:\"owner,omitempty\"`\n}\n\n// AuthStorageConfig contains auth storage settings\ntype AuthStorageConfig struct {\n\tType string `yaml:\"type\"`\n\tFile string `yaml:\"file\"`\n}\n\n// AuthOwnerConfig contains initial admin user settings\ntype AuthOwnerConfig struct {\n\tEmail string `yaml:\"email\"`\n\tPassword string `yaml:\"password\"`\n}\n\n// HostConfig represents a STUN/TURN/Signal host\ntype HostConfig struct {\n\tURI string `yaml:\"uri\"`\n\tProto string `yaml:\"proto,omitempty\"` // udp, dtls, tcp, http, https - defaults based on URI scheme\n\tUsername string `yaml:\"username,omitempty\"`\n\tPassword string `yaml:\"password,omitempty\"`\n}\n\n// RelaysConfig contains external relay server settings for clients\ntype RelaysConfig struct {\n\tAddresses []string `yaml:\"addresses\"`\n\tCredentialsTTL string `yaml:\"credentialsTTL\"`\n\tSecret string `yaml:\"secret\"`\n}\n\n// StoreConfig contains database settings\ntype StoreConfig struct {\n\tEngine string `yaml:\"engine\"`\n\tEncryptionKey string `yaml:\"encryptionKey\"`\n\tDSN string `yaml:\"dsn\"` // Connection string for postgres or mysql engines\n\tFile string `yaml:\"file\"` // SQLite database file path (optional, defaults to dataDir)\n}\n\n// ReverseProxyConfig contains reverse proxy settings\ntype ReverseProxyConfig struct {\n\tTrustedHTTPProxies []string `yaml:\"trustedHTTPProxies\"`\n\tTrustedHTTPProxiesCount uint `yaml:\"trustedHTTPProxiesCount\"`\n\tTrustedPeers []string `yaml:\"trustedPeers\"`\n}\n\n// DefaultConfig returns a CombinedConfig with default values\nfunc DefaultConfig() *CombinedConfig {\n\treturn &CombinedConfig{\n\t\tServer: ServerConfig{\n\t\t\tListenAddress: \":443\",\n\t\t\tMetricsPort: 9090,\n\t\t\tHealthcheckAddress: \":9000\",\n\t\t\tLogLevel: \"info\",\n\t\t\tLogFile: \"console\",\n\t\t\tStunPorts: []int{3478},\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t\tRelay: RelayConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\tStun: StunConfig{\n\t\t\t\tEnabled: false,\n\t\t\t\tPorts: []int{3478},\n\t\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\t},\n\t\t},\n\t\tSignal: SignalConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t},\n\t\tManagement: ManagementConfig{\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRelays: RelaysConfig{\n\t\t\t\tCredentialsTTL: \"12h\",\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t}\n}\n\n// hasRequiredSettings returns true if the configuration has the required server settings\nfunc (c *CombinedConfig) hasRequiredSettings() bool {\n\treturn c.Server.ExposedAddress != \"\"\n}\n\n// parseExposedAddress extracts protocol, host, and host:port from the exposed address\n// Input format: \"https://example.com:443\" or \"http://example.com:8080\" or \"example.com:443\"\n// Returns: protocol (\"https\" or \"http\"), hostname only, and host:port\nfunc parseExposedAddress(exposedAddress string) (protocol, hostname, hostPort string) {\n\t// Default to https if no protocol specified\n\tprotocol = \"https\"\n\thostPort = exposedAddress\n\n\t// Check for protocol prefix\n\tif strings.HasPrefix(exposedAddress, \"https://\") {\n\t\tprotocol = \"https\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"https://\")\n\t} else if strings.HasPrefix(exposedAddress, \"http://\") {\n\t\tprotocol = \"http\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"http://\")\n\t}\n\n\t// Extract hostname (without port)\n\thostname = hostPort\n\tif host, _, err := net.SplitHostPort(hostPort); err == nil {\n\t\thostname = host\n\t}\n\n\treturn protocol, hostname, hostPort\n}\n\n// ApplySimplifiedDefaults populates internal relay/signal/management configs from server settings.\n// Management is always enabled. Signal, Relay, and STUN are enabled unless external\n// overrides are configured (server.signalUri, server.relays, server.stuns).\nfunc (c *CombinedConfig) ApplySimplifiedDefaults() {\n\tif !c.hasRequiredSettings() {\n\t\treturn\n\t}\n\n\t// Parse exposed address to extract protocol and hostname\n\texposedProto, exposedHost, exposedHostPort := parseExposedAddress(c.Server.ExposedAddress)\n\n\t// Check for external service overrides\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\thasExternalSignal := c.Server.SignalURI != \"\"\n\thasExternalStuns := len(c.Server.Stuns) > 0\n\n\t// Default stunPorts to [3478] if not specified and no external STUN\n\tif len(c.Server.StunPorts) == 0 && !hasExternalStuns {\n\t\tc.Server.StunPorts = []int{3478}\n\t}\n\n\tc.applyRelayDefaults(exposedProto, exposedHostPort, hasExternalRelay, hasExternalStuns)\n\tc.applySignalDefaults(hasExternalSignal)\n\tc.applyManagementDefaults(exposedHost)\n\n\t// Auto-configure client settings (stuns, relays, signalUri)\n\tc.autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort, hasExternalStuns, hasExternalRelay, hasExternalSignal)\n}\n\n// applyRelayDefaults configures the relay service if no external relay is configured.\nfunc (c *CombinedConfig) applyRelayDefaults(exposedProto, exposedHostPort string, hasExternalRelay, hasExternalStuns bool) {\n\tif hasExternalRelay {\n\t\treturn\n\t}\n\n\tc.Relay.Enabled = true\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\tc.Relay.ExposedAddress = fmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort)\n\tc.Relay.AuthSecret = c.Server.AuthSecret\n\tif c.Relay.LogLevel == \"\" {\n\t\tc.Relay.LogLevel = c.Server.LogLevel\n\t}\n\n\t// Enable local STUN only if no external STUN servers and stunPorts are configured\n\tif !hasExternalStuns && len(c.Server.StunPorts) > 0 {\n\t\tc.Relay.Stun.Enabled = true\n\t\tc.Relay.Stun.Ports = c.Server.StunPorts\n\t\tif c.Relay.Stun.LogLevel == \"\" {\n\t\t\tc.Relay.Stun.LogLevel = c.Server.LogLevel\n\t\t}\n\t}\n}\n\n// applySignalDefaults configures the signal service if no external signal is configured.\nfunc (c *CombinedConfig) applySignalDefaults(hasExternalSignal bool) {\n\tif hasExternalSignal {\n\t\treturn\n\t}\n\n\tc.Signal.Enabled = true\n\tif c.Signal.LogLevel == \"\" {\n\t\tc.Signal.LogLevel = c.Server.LogLevel\n\t}\n}\n\n// applyManagementDefaults configures the management service (always enabled).\nfunc (c *CombinedConfig) applyManagementDefaults(exposedHost string) {\n\tc.Management.Enabled = true\n\tif c.Management.LogLevel == \"\" {\n\t\tc.Management.LogLevel = c.Server.LogLevel\n\t}\n\tif c.Management.DataDir == \"\" || c.Management.DataDir == \"/var/lib/netbird/\" {\n\t\tc.Management.DataDir = c.Server.DataDir\n\t}\n\tc.Management.DnsDomain = exposedHost\n\tc.Management.DisableAnonymousMetrics = c.Server.DisableAnonymousMetrics\n\tc.Management.DisableGeoliteUpdate = c.Server.DisableGeoliteUpdate\n\t// Copy auth config from server if management auth issuer is not set\n\tif c.Management.Auth.Issuer == \"\" && c.Server.Auth.Issuer != \"\" {\n\t\tc.Management.Auth = c.Server.Auth\n\t}\n\n\t// Copy store config from server if not set\n\tif c.Management.Store.Engine == \"\" || c.Management.Store.Engine == \"sqlite\" {\n\t\tif c.Server.Store.Engine != \"\" {\n\t\t\tc.Management.Store = c.Server.Store\n\t\t}\n\t}\n\n\t// Copy reverse proxy config from server\n\tif len(c.Server.ReverseProxy.TrustedHTTPProxies) > 0 || c.Server.ReverseProxy.TrustedHTTPProxiesCount > 0 || len(c.Server.ReverseProxy.TrustedPeers) > 0 {\n\t\tc.Management.ReverseProxy = c.Server.ReverseProxy\n\t}\n}\n\n// autoConfigureClientSettings sets up STUN/relay/signal URIs for clients\n// External overrides from server config take precedence over auto-generated values\nfunc (c *CombinedConfig) autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort string, hasExternalStuns, hasExternalRelay, hasExternalSignal bool) {\n\t// Determine relay protocol from exposed protocol\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\n\t// Configure STUN servers for clients\n\tif hasExternalStuns {\n\t\t// Use external STUN servers from server config\n\t\tc.Management.Stuns = c.Server.Stuns\n\t} else if len(c.Server.StunPorts) > 0 && len(c.Management.Stuns) == 0 {\n\t\t// Auto-configure local STUN servers for all ports\n\t\tfor _, port := range c.Server.StunPorts {\n\t\t\tc.Management.Stuns = append(c.Management.Stuns, HostConfig{\n\t\t\t\tURI: fmt.Sprintf(\"stun:%s:%d\", exposedHost, port),\n\t\t\t})\n\t\t}\n\t}\n\n\t// Configure relay for clients\n\tif hasExternalRelay {\n\t\t// Use external relay config from server\n\t\tc.Management.Relays = c.Server.Relays\n\t} else if len(c.Management.Relays.Addresses) == 0 {\n\t\t// Auto-configure local relay\n\t\tc.Management.Relays.Addresses = []string{\n\t\t\tfmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort),\n\t\t}\n\t}\n\tif c.Management.Relays.Secret == \"\" {\n\t\tc.Management.Relays.Secret = c.Server.AuthSecret\n\t}\n\tif c.Management.Relays.CredentialsTTL == \"\" {\n\t\tc.Management.Relays.CredentialsTTL = \"12h\"\n\t}\n\n\t// Configure signal for clients\n\tif hasExternalSignal {\n\t\t// Use external signal URI from server config\n\t\tc.Management.SignalURI = c.Server.SignalURI\n\t} else if c.Management.SignalURI == \"\" {\n\t\t// Auto-configure local signal\n\t\tc.Management.SignalURI = fmt.Sprintf(\"%s://%s\", exposedProto, exposedHostPort)\n\t}\n}\n\n// LoadConfig loads configuration from a YAML file\nfunc LoadConfig(configPath string) (*CombinedConfig, error) {\n\tcfg := DefaultConfig()\n\n\tif configPath == \"\" {\n\t\treturn cfg, nil\n\t}\n\n\tdata, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read config file: %w\", err)\n\t}\n\n\tif err := yaml.Unmarshal(data, cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse config file: %w\", err)\n\t}\n\n\t// Populate internal configs from server settings\n\tcfg.ApplySimplifiedDefaults()\n\n\treturn cfg, nil\n}\n\n// Validate validates the configuration\nfunc (c *CombinedConfig) Validate() error {\n\tif c.Server.ExposedAddress == \"\" {\n\t\treturn fmt.Errorf(\"server.exposedAddress is required\")\n\t}\n\tif c.Server.DataDir == \"\" {\n\t\treturn fmt.Errorf(\"server.dataDir is required\")\n\t}\n\n\t// Validate STUN ports\n\tseen := make(map[int]bool)\n\tfor _, port := range c.Server.StunPorts {\n\t\tif port <= 0 || port > 65535 {\n\t\t\treturn fmt.Errorf(\"invalid server.stunPorts value %d: must be between 1 and 65535\", port)\n\t\t}\n\t\tif seen[port] {\n\t\t\treturn fmt.Errorf(\"duplicate STUN port %d in server.stunPorts\", port)\n\t\t}\n\t\tseen[port] = true\n\t}\n\n\t// authSecret is required only if running local relay (no external relay configured)\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\tif !hasExternalRelay && c.Server.AuthSecret == \"\" {\n\t\treturn fmt.Errorf(\"server.authSecret is required when running local relay\")\n\t}\n\n\treturn nil\n}\n\n// HasTLSCert returns true if TLS certificate files are configured\nfunc (c *CombinedConfig) HasTLSCert() bool {\n\treturn c.Server.TLS.CertFile != \"\" && c.Server.TLS.KeyFile != \"\"\n}\n\n// HasLetsEncrypt returns true if Let's Encrypt is configured\nfunc (c *CombinedConfig) HasLetsEncrypt() bool {\n\treturn c.Server.TLS.LetsEncrypt.Enabled &&\n\t\tc.Server.TLS.LetsEncrypt.DataDir != \"\" &&\n\t\tlen(c.Server.TLS.LetsEncrypt.Domains) > 0\n}\n\n// parseExplicitProtocol parses an explicit protocol string to nbconfig.Protocol\nfunc parseExplicitProtocol(proto string) (nbconfig.Protocol, bool) {\n\tswitch strings.ToLower(proto) {\n\tcase \"udp\":\n\t\treturn nbconfig.UDP, true\n\tcase \"dtls\":\n\t\treturn nbconfig.DTLS, true\n\tcase \"tcp\":\n\t\treturn nbconfig.TCP, true\n\tcase \"http\":\n\t\treturn nbconfig.HTTP, true\n\tcase \"https\":\n\t\treturn nbconfig.HTTPS, true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\n// parseStunProtocol determines protocol for STUN/TURN servers.\n// stun: → UDP, stuns: → DTLS, turn: → UDP, turns: → DTLS\n// Explicit proto overrides URI scheme. Defaults to UDP.\nfunc parseStunProtocol(uri, proto string) nbconfig.Protocol {\n\tif proto != \"\" {\n\t\tif p, ok := parseExplicitProtocol(proto); ok {\n\t\t\treturn p\n\t\t}\n\t}\n\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"stuns:\"):\n\t\treturn nbconfig.DTLS\n\tcase strings.HasPrefix(uri, \"turns:\"):\n\t\treturn nbconfig.DTLS\n\tdefault:\n\t\t// stun:, turn:, or no scheme - default to UDP\n\t\treturn nbconfig.UDP\n\t}\n}\n\n// parseSignalProtocol determines protocol for Signal servers.\n// https:// → HTTPS, http:// → HTTP. Defaults to HTTPS.\nfunc parseSignalProtocol(uri string) nbconfig.Protocol {\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"http://\"):\n\t\treturn nbconfig.HTTP\n\tdefault:\n\t\t// https:// or no scheme - default to HTTPS\n\t\treturn nbconfig.HTTPS\n\t}\n}\n\n// stripSignalProtocol removes the protocol prefix from a signal URI.\n// Returns just the host:port (e.g., \"selfhosted2.demo.netbird.io:443\").\nfunc stripSignalProtocol(uri string) string {\n\turi = strings.TrimPrefix(uri, \"https://\")\n\turi = strings.TrimPrefix(uri, \"http://\")\n\treturn uri\n}\n\nfunc buildRelayConfig(relays RelaysConfig) (*nbconfig.Relay, error) {\n\tvar ttl time.Duration\n\tif relays.CredentialsTTL != \"\" {\n\t\tvar err error\n\t\tttl, err = time.ParseDuration(relays.CredentialsTTL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid relay credentials TTL %q: %w\", relays.CredentialsTTL, err)\n\t\t}\n\t}\n\treturn &nbconfig.Relay{\n\t\tAddresses: relays.Addresses,\n\t\tCredentialsTTL: util.Duration{Duration: ttl},\n\t\tSecret: relays.Secret,\n\t}, nil\n}\n\n// buildEmbeddedIdPConfig builds the embedded IdP configuration.\n// authStore overrides auth.storage when set.\nfunc (c *CombinedConfig) buildEmbeddedIdPConfig(mgmt ManagementConfig) (*idp.EmbeddedIdPConfig, error) {\n\tauthStorageType := mgmt.Auth.Storage.Type\n\tauthStorageDSN := c.Server.AuthStore.DSN\n\tif c.Server.AuthStore.Engine != \"\" {\n\t\tauthStorageType = c.Server.AuthStore.Engine\n\t}\n\tif authStorageType == \"\" {\n\t\tauthStorageType = \"sqlite3\"\n\t}\n\tauthStorageFile := \"\"\n\tif authStorageType == \"postgres\" {\n\t\tif authStorageDSN == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"authStore.dsn is required when authStore.engine is postgres\")\n\t\t}\n\t} else {\n\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n\t\tif c.Server.AuthStore.File != \"\" {\n\t\t\tauthStorageFile = c.Server.AuthStore.File\n\t\t}\n\t}\n\n\tcfg := &idp.EmbeddedIdPConfig{\n\t\tEnabled: true,\n\t\tIssuer: mgmt.Auth.Issuer,\n\t\tLocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,\n\t\tSignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,\n\t\tStorage: idp.EmbeddedStorageConfig{\n\t\t\tType: authStorageType,\n\t\t\tConfig: idp.EmbeddedStorageTypeConfig{\n\t\t\t\tFile: authStorageFile,\n\t\t\t\tDSN: authStorageDSN,\n\t\t\t},\n\t\t},\n\t\tDashboardRedirectURIs: mgmt.Auth.DashboardRedirectURIs,\n\t\tCLIRedirectURIs: mgmt.Auth.CLIRedirectURIs,\n\t}\n\n\tif mgmt.Auth.Owner != nil && mgmt.Auth.Owner.Email != \"\" {\n\t\tcfg.Owner = &idp.OwnerConfig{\n\t\t\tEmail: mgmt.Auth.Owner.Email,\n\t\t\tHash: mgmt.Auth.Owner.Password,\n\t\t}\n\t}\n\n\treturn cfg, nil\n}\n\n// ToManagementConfig converts CombinedConfig to management server config\nfunc (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {\n\tmgmt := c.Management\n\n\t// Build STUN hosts\n\tvar stuns []*nbconfig.Host\n\tfor _, s := range mgmt.Stuns {\n\t\tstuns = append(stuns, &nbconfig.Host{\n\t\t\tURI: s.URI,\n\t\t\tProto: parseStunProtocol(s.URI, s.Proto),\n\t\t\tUsername: s.Username,\n\t\t\tPassword: s.Password,\n\t\t})\n\t}\n\n\t// Build relay config\n\tvar relayConfig *nbconfig.Relay\n\tif len(mgmt.Relays.Addresses) > 0 || mgmt.Relays.Secret != \"\" {\n\t\trelay, err := buildRelayConfig(mgmt.Relays)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trelayConfig = relay\n\t}\n\n\t// Build signal config\n\tvar signalConfig *nbconfig.Host\n\tif mgmt.SignalURI != \"\" {\n\t\tsignalConfig = &nbconfig.Host{\n\t\t\tURI: stripSignalProtocol(mgmt.SignalURI),\n\t\t\tProto: parseSignalProtocol(mgmt.SignalURI),\n\t\t}\n\t}\n\n\t// Build store config\n\tstoreConfig := nbconfig.StoreConfig{\n\t\tEngine: types.Engine(mgmt.Store.Engine),\n\t}\n\n\t// Build reverse proxy config\n\treverseProxy := nbconfig.ReverseProxy{\n\t\tTrustedHTTPProxiesCount: mgmt.ReverseProxy.TrustedHTTPProxiesCount,\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedHTTPProxies {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedHTTPProxies = append(reverseProxy.TrustedHTTPProxies, prefix)\n\t\t}\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedPeers {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedPeers = append(reverseProxy.TrustedPeers, prefix)\n\t\t}\n\t}\n\n\t// Build HTTP config (required, even if empty)\n\thttpConfig := &nbconfig.HttpServerConfig{}\n\n\t// Build embedded IDP config (always enabled in combined server)\n\tembeddedIdP, err := c.buildEmbeddedIdPConfig(mgmt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Set HTTP config fields for embedded IDP\n\thttpConfig.AuthIssuer = mgmt.Auth.Issuer\n\thttpConfig.AuthAudience = \"netbird-dashboard\"\n\thttpConfig.AuthClientID = httpConfig.AuthAudience\n\thttpConfig.CLIAuthAudience = \"netbird-cli\"\n\thttpConfig.AuthUserIDClaim = \"sub\"\n\thttpConfig.AuthKeysLocation = mgmt.Auth.Issuer + \"/keys\"\n\thttpConfig.OIDCConfigEndpoint = mgmt.Auth.Issuer + \"/.well-known/openid-configuration\"\n\thttpConfig.IdpSignKeyRefreshEnabled = mgmt.Auth.SignKeyRefreshEnabled\n\tcallbackURL := strings.TrimSuffix(httpConfig.AuthIssuer, \"/oauth2\")\n\thttpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull\n\n\treturn &nbconfig.Config{\n\t\tStuns: stuns,\n\t\tRelay: relayConfig,\n\t\tSignal: signalConfig,\n\t\tDatadir: mgmt.DataDir,\n\t\tDataStoreEncryptionKey: mgmt.Store.EncryptionKey,\n\t\tHttpConfig: httpConfig,\n\t\tStoreConfig: storeConfig,\n\t\tReverseProxy: reverseProxy,\n\t\tDisableDefaultPolicy: mgmt.DisableDefaultPolicy,\n\t\tEmbeddedIdP: embeddedIdP,\n\t}, nil\n}\n\n// ApplyEmbeddedIdPConfig applies embedded IdP configuration to the management config.\n// This mirrors the logic in management/cmd/management.go ApplyEmbeddedIdPConfig.\nfunc ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config, mgmtPort int, disableSingleAccMode bool) error {\n\tif cfg.EmbeddedIdP == nil || !cfg.EmbeddedIdP.Enabled {\n\t\treturn nil\n\t}\n\n\t// Embedded IdP requires single account mode\n\tif disableSingleAccMode {\n\t\treturn fmt.Errorf(\"embedded IdP requires single account mode; multiple account mode is not supported with embedded IdP\")\n\t}\n\n\t// Set LocalAddress for embedded IdP, used for internal JWT validation\n\tcfg.EmbeddedIdP.LocalAddress = fmt.Sprintf(\"localhost:%d\", mgmtPort)\n\n\t// Set storage defaults based on Datadir\n\tif cfg.EmbeddedIdP.Storage.Type == \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Type = \"sqlite3\"\n\t}\n\tif cfg.EmbeddedIdP.Storage.Config.File == \"\" && cfg.Datadir != \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")\n\t}\n\n\tissuer := cfg.EmbeddedIdP.Issuer\n\n\t// Ensure HttpConfig exists\n\tif cfg.HttpConfig == nil {\n\t\tcfg.HttpConfig = &nbconfig.HttpServerConfig{}\n\t}\n\n\t// Set HttpConfig values from EmbeddedIdP\n\tcfg.HttpConfig.AuthIssuer = issuer\n\tcfg.HttpConfig.AuthAudience = \"netbird-dashboard\"\n\tcfg.HttpConfig.CLIAuthAudience = \"netbird-cli\"\n\tcfg.HttpConfig.AuthUserIDClaim = \"sub\"\n\tcfg.HttpConfig.AuthKeysLocation = issuer + \"/keys\"\n\tcfg.HttpConfig.OIDCConfigEndpoint = issuer + \"/.well-known/openid-configuration\"\n\tcfg.HttpConfig.IdpSignKeyRefreshEnabled = true\n\n\treturn nil\n}\n\n// EnsureEncryptionKey generates an encryption key if not set.\n// Unlike management server, we don't write back to the config file.\nfunc EnsureEncryptionKey(ctx context.Context, cfg *nbconfig.Config) error {\n\tif cfg.DataStoreEncryptionKey != \"\" {\n\t\treturn nil\n\t}\n\n\tlog.WithContext(ctx).Infof(\"DataStoreEncryptionKey is not set, generating a new key\")\n\tkey, err := crypt.GenerateKey()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate datastore encryption key: %v\", err)\n\t}\n\tcfg.DataStoreEncryptionKey = key\n\tkeyPreview := key[:8] + \"...\"\n\tlog.WithContext(ctx).Warnf(\"DataStoreEncryptionKey generated (%s); add it to your config file under 'server.store.encryptionKey' to persist across restarts\", keyPreview)\n\n\treturn nil\n}\n\n// LogConfigInfo logs informational messages about the loaded configuration\nfunc LogConfigInfo(cfg *nbconfig.Config) {\n\tif cfg.EmbeddedIdP != nil && cfg.EmbeddedIdP.Enabled {\n\t\tlog.Infof(\"running with the embedded IdP: %v\", cfg.EmbeddedIdP.Issuer)\n\t}\n\tif cfg.Relay != nil {\n\t\tlog.Infof(\"Relay addresses: %v\", cfg.Relay.Addresses)\n\t}\n}\n","structuredPatch":[{"oldStart":7,"oldLines":6,"newStart":7,"newLines":7,"lines":[" \"net/netip\""," \"os\""," \"path\"","+ \"path/filepath\""," \"strings\""," \"time\""," "]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"e763c195-a5d3-4a66-9db7-6c641b26a84c"} +{"parentUuid":"fc19682f-e0c0-4353-8045-b1edb0e4cf37","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01MDAX8vS7eaS6avRrQeDWGm","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_017XNsx8dqfxoNFxabpY1m34","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go","old_string":"\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n\t\tif c.Server.AuthStore.File != \"\" {\n\t\t\tauthStorageFile = c.Server.AuthStore.File\n\t\t}","new_string":"\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n\t\tif c.Server.AuthStore.File != \"\" {\n\t\t\tauthStorageFile = c.Server.AuthStore.File\n\t\t\tif !filepath.IsAbs(authStorageFile) {\n\t\t\t\tauthStorageFile = filepath.Join(mgmt.DataDir, authStorageFile)\n\t\t\t}\n\t\t}"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":77818,"cache_read_input_tokens":10426,"output_tokens":280,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":77818,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYfs9Tg8csHsWy3xobnJG","type":"assistant","uuid":"9d500222-898a-4d81-8081-a69583b5c5c0","timestamp":"2026-03-03T08:54:28.318Z"} +{"parentUuid":"9d500222-898a-4d81-8081-a69583b5c5c0","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_017XNsx8dqfxoNFxabpY1m34","toolUseID":"toolu_017XNsx8dqfxoNFxabpY1m34","timestamp":"2026-03-03T09:07:03.970Z","uuid":"9598d876-4bc0-46bb-a0cf-8a4e8a9bc2aa"} +{"parentUuid":"9d500222-898a-4d81-8081-a69583b5c5c0","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_017XNsx8dqfxoNFxabpY1m34","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go has been updated successfully."}]},"uuid":"204c217e-8a67-4b4f-8196-04c1753fc24c","timestamp":"2026-03-03T09:07:03.964Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/combined/cmd/config.go","oldString":"package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/netbirdio/netbird/management/server/idp\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n\n\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n)\n\n// CombinedConfig is the root configuration for the combined server.\n// The combined server is primarily a Management server with optional embedded\n// Signal, Relay, and STUN services.\n//\n// Architecture:\n// - Management: Always runs locally (this IS the management server)\n// - Signal: Runs locally by default; disabled if server.signalUri is set\n// - Relay: Runs locally by default; disabled if server.relays is set\n// - STUN: Runs locally on port 3478 by default; disabled if server.stuns is set\n//\n// All user-facing settings are under \"server\". The relay/signal/management\n// fields are internal and populated automatically from server settings.\ntype CombinedConfig struct {\n\tServer ServerConfig `yaml:\"server\"`\n\n\t// Internal configs - populated from Server settings, not user-configurable\n\tRelay RelayConfig `yaml:\"-\"`\n\tSignal SignalConfig `yaml:\"-\"`\n\tManagement ManagementConfig `yaml:\"-\"`\n}\n\n// ServerConfig contains server-wide settings\n// In simplified mode, this contains all configuration\ntype ServerConfig struct {\n\tListenAddress string `yaml:\"listenAddress\"`\n\tMetricsPort int `yaml:\"metricsPort\"`\n\tHealthcheckAddress string `yaml:\"healthcheckAddress\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tLogFile string `yaml:\"logFile\"`\n\tTLS TLSConfig `yaml:\"tls\"`\n\n\t// Simplified config fields (used when relay/signal/management sections are omitted)\n\tExposedAddress string `yaml:\"exposedAddress\"` // Public address with protocol (e.g., \"https://example.com:443\")\n\tStunPorts []int `yaml:\"stunPorts\"` // STUN ports (empty to disable local STUN)\n\tAuthSecret string `yaml:\"authSecret\"` // Shared secret for relay authentication\n\tDataDir string `yaml:\"dataDir\"` // Data directory for all services\n\n\t// External service overrides (simplified mode)\n\t// When these are set, the corresponding local service is NOT started\n\t// and these values are used for client configuration instead\n\tStuns []HostConfig `yaml:\"stuns\"` // External STUN servers (disables local STUN)\n\tRelays RelaysConfig `yaml:\"relays\"` // External relay servers (disables local relay)\n\tSignalURI string `yaml:\"signalUri\"` // External signal server (disables local signal)\n\n\t// Management settings (simplified mode)\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tActivityStore StoreConfig `yaml:\"activityStore\"`\n\tAuthStore StoreConfig `yaml:\"authStore\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// TLSConfig contains TLS/HTTPS settings\ntype TLSConfig struct {\n\tCertFile string `yaml:\"certFile\"`\n\tKeyFile string `yaml:\"keyFile\"`\n\tLetsEncrypt LetsEncryptConfig `yaml:\"letsencrypt\"`\n}\n\n// LetsEncryptConfig contains Let's Encrypt settings\ntype LetsEncryptConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDomains []string `yaml:\"domains\"`\n\tEmail string `yaml:\"email\"`\n\tAWSRoute53 bool `yaml:\"awsRoute53\"`\n}\n\n// RelayConfig contains relay service settings\ntype RelayConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tExposedAddress string `yaml:\"exposedAddress\"`\n\tAuthSecret string `yaml:\"authSecret\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tStun StunConfig `yaml:\"stun\"`\n}\n\n// StunConfig contains embedded STUN service settings\ntype StunConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tPorts []int `yaml:\"ports\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// SignalConfig contains signal service settings\ntype SignalConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// ManagementConfig contains management service settings\ntype ManagementConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDnsDomain string `yaml:\"dnsDomain\"`\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tDisableDefaultPolicy bool `yaml:\"disableDefaultPolicy\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStuns []HostConfig `yaml:\"stuns\"`\n\tRelays RelaysConfig `yaml:\"relays\"`\n\tSignalURI string `yaml:\"signalUri\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// AuthConfig contains authentication/identity provider settings\ntype AuthConfig struct {\n\tIssuer string `yaml:\"issuer\"`\n\tLocalAuthDisabled bool `yaml:\"localAuthDisabled\"`\n\tSignKeyRefreshEnabled bool `yaml:\"signKeyRefreshEnabled\"`\n\tStorage AuthStorageConfig `yaml:\"storage\"`\n\tDashboardRedirectURIs []string `yaml:\"dashboardRedirectURIs\"`\n\tCLIRedirectURIs []string `yaml:\"cliRedirectURIs\"`\n\tOwner *AuthOwnerConfig `yaml:\"owner,omitempty\"`\n}\n\n// AuthStorageConfig contains auth storage settings\ntype AuthStorageConfig struct {\n\tType string `yaml:\"type\"`\n\tFile string `yaml:\"file\"`\n}\n\n// AuthOwnerConfig contains initial admin user settings\ntype AuthOwnerConfig struct {\n\tEmail string `yaml:\"email\"`\n\tPassword string `yaml:\"password\"`\n}\n\n// HostConfig represents a STUN/TURN/Signal host\ntype HostConfig struct {\n\tURI string `yaml:\"uri\"`\n\tProto string `yaml:\"proto,omitempty\"` // udp, dtls, tcp, http, https - defaults based on URI scheme\n\tUsername string `yaml:\"username,omitempty\"`\n\tPassword string `yaml:\"password,omitempty\"`\n}\n\n// RelaysConfig contains external relay server settings for clients\ntype RelaysConfig struct {\n\tAddresses []string `yaml:\"addresses\"`\n\tCredentialsTTL string `yaml:\"credentialsTTL\"`\n\tSecret string `yaml:\"secret\"`\n}\n\n// StoreConfig contains database settings\ntype StoreConfig struct {\n\tEngine string `yaml:\"engine\"`\n\tEncryptionKey string `yaml:\"encryptionKey\"`\n\tDSN string `yaml:\"dsn\"` // Connection string for postgres or mysql engines\n\tFile string `yaml:\"file\"` // SQLite database file path (optional, defaults to dataDir)\n}\n\n// ReverseProxyConfig contains reverse proxy settings\ntype ReverseProxyConfig struct {\n\tTrustedHTTPProxies []string `yaml:\"trustedHTTPProxies\"`\n\tTrustedHTTPProxiesCount uint `yaml:\"trustedHTTPProxiesCount\"`\n\tTrustedPeers []string `yaml:\"trustedPeers\"`\n}\n\n// DefaultConfig returns a CombinedConfig with default values\nfunc DefaultConfig() *CombinedConfig {\n\treturn &CombinedConfig{\n\t\tServer: ServerConfig{\n\t\t\tListenAddress: \":443\",\n\t\t\tMetricsPort: 9090,\n\t\t\tHealthcheckAddress: \":9000\",\n\t\t\tLogLevel: \"info\",\n\t\t\tLogFile: \"console\",\n\t\t\tStunPorts: []int{3478},\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t\tRelay: RelayConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\tStun: StunConfig{\n\t\t\t\tEnabled: false,\n\t\t\t\tPorts: []int{3478},\n\t\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\t},\n\t\t},\n\t\tSignal: SignalConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t},\n\t\tManagement: ManagementConfig{\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRelays: RelaysConfig{\n\t\t\t\tCredentialsTTL: \"12h\",\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t}\n}\n\n// hasRequiredSettings returns true if the configuration has the required server settings\nfunc (c *CombinedConfig) hasRequiredSettings() bool {\n\treturn c.Server.ExposedAddress != \"\"\n}\n\n// parseExposedAddress extracts protocol, host, and host:port from the exposed address\n// Input format: \"https://example.com:443\" or \"http://example.com:8080\" or \"example.com:443\"\n// Returns: protocol (\"https\" or \"http\"), hostname only, and host:port\nfunc parseExposedAddress(exposedAddress string) (protocol, hostname, hostPort string) {\n\t// Default to https if no protocol specified\n\tprotocol = \"https\"\n\thostPort = exposedAddress\n\n\t// Check for protocol prefix\n\tif strings.HasPrefix(exposedAddress, \"https://\") {\n\t\tprotocol = \"https\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"https://\")\n\t} else if strings.HasPrefix(exposedAddress, \"http://\") {\n\t\tprotocol = \"http\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"http://\")\n\t}\n\n\t// Extract hostname (without port)\n\thostname = hostPort\n\tif host, _, err := net.SplitHostPort(hostPort); err == nil {\n\t\thostname = host\n\t}\n\n\treturn protocol, hostname, hostPort\n}\n\n// ApplySimplifiedDefaults populates internal relay/signal/management configs from server settings.\n// Management is always enabled. Signal, Relay, and STUN are enabled unless external\n// overrides are configured (server.signalUri, server.relays, server.stuns).\nfunc (c *CombinedConfig) ApplySimplifiedDefaults() {\n\tif !c.hasRequiredSettings() {\n\t\treturn\n\t}\n\n\t// Parse exposed address to extract protocol and hostname\n\texposedProto, exposedHost, exposedHostPort := parseExposedAddress(c.Server.ExposedAddress)\n\n\t// Check for external service overrides\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\thasExternalSignal := c.Server.SignalURI != \"\"\n\thasExternalStuns := len(c.Server.Stuns) > 0\n\n\t// Default stunPorts to [3478] if not specified and no external STUN\n\tif len(c.Server.StunPorts) == 0 && !hasExternalStuns {\n\t\tc.Server.StunPorts = []int{3478}\n\t}\n\n\tc.applyRelayDefaults(exposedProto, exposedHostPort, hasExternalRelay, hasExternalStuns)\n\tc.applySignalDefaults(hasExternalSignal)\n\tc.applyManagementDefaults(exposedHost)\n\n\t// Auto-configure client settings (stuns, relays, signalUri)\n\tc.autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort, hasExternalStuns, hasExternalRelay, hasExternalSignal)\n}\n\n// applyRelayDefaults configures the relay service if no external relay is configured.\nfunc (c *CombinedConfig) applyRelayDefaults(exposedProto, exposedHostPort string, hasExternalRelay, hasExternalStuns bool) {\n\tif hasExternalRelay {\n\t\treturn\n\t}\n\n\tc.Relay.Enabled = true\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\tc.Relay.ExposedAddress = fmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort)\n\tc.Relay.AuthSecret = c.Server.AuthSecret\n\tif c.Relay.LogLevel == \"\" {\n\t\tc.Relay.LogLevel = c.Server.LogLevel\n\t}\n\n\t// Enable local STUN only if no external STUN servers and stunPorts are configured\n\tif !hasExternalStuns && len(c.Server.StunPorts) > 0 {\n\t\tc.Relay.Stun.Enabled = true\n\t\tc.Relay.Stun.Ports = c.Server.StunPorts\n\t\tif c.Relay.Stun.LogLevel == \"\" {\n\t\t\tc.Relay.Stun.LogLevel = c.Server.LogLevel\n\t\t}\n\t}\n}\n\n// applySignalDefaults configures the signal service if no external signal is configured.\nfunc (c *CombinedConfig) applySignalDefaults(hasExternalSignal bool) {\n\tif hasExternalSignal {\n\t\treturn\n\t}\n\n\tc.Signal.Enabled = true\n\tif c.Signal.LogLevel == \"\" {\n\t\tc.Signal.LogLevel = c.Server.LogLevel\n\t}\n}\n\n// applyManagementDefaults configures the management service (always enabled).\nfunc (c *CombinedConfig) applyManagementDefaults(exposedHost string) {\n\tc.Management.Enabled = true\n\tif c.Management.LogLevel == \"\" {\n\t\tc.Management.LogLevel = c.Server.LogLevel\n\t}\n\tif c.Management.DataDir == \"\" || c.Management.DataDir == \"/var/lib/netbird/\" {\n\t\tc.Management.DataDir = c.Server.DataDir\n\t}\n\tc.Management.DnsDomain = exposedHost\n\tc.Management.DisableAnonymousMetrics = c.Server.DisableAnonymousMetrics\n\tc.Management.DisableGeoliteUpdate = c.Server.DisableGeoliteUpdate\n\t// Copy auth config from server if management auth issuer is not set\n\tif c.Management.Auth.Issuer == \"\" && c.Server.Auth.Issuer != \"\" {\n\t\tc.Management.Auth = c.Server.Auth\n\t}\n\n\t// Copy store config from server if not set\n\tif c.Management.Store.Engine == \"\" || c.Management.Store.Engine == \"sqlite\" {\n\t\tif c.Server.Store.Engine != \"\" {\n\t\t\tc.Management.Store = c.Server.Store\n\t\t}\n\t}\n\n\t// Copy reverse proxy config from server\n\tif len(c.Server.ReverseProxy.TrustedHTTPProxies) > 0 || c.Server.ReverseProxy.TrustedHTTPProxiesCount > 0 || len(c.Server.ReverseProxy.TrustedPeers) > 0 {\n\t\tc.Management.ReverseProxy = c.Server.ReverseProxy\n\t}\n}\n\n// autoConfigureClientSettings sets up STUN/relay/signal URIs for clients\n// External overrides from server config take precedence over auto-generated values\nfunc (c *CombinedConfig) autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort string, hasExternalStuns, hasExternalRelay, hasExternalSignal bool) {\n\t// Determine relay protocol from exposed protocol\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\n\t// Configure STUN servers for clients\n\tif hasExternalStuns {\n\t\t// Use external STUN servers from server config\n\t\tc.Management.Stuns = c.Server.Stuns\n\t} else if len(c.Server.StunPorts) > 0 && len(c.Management.Stuns) == 0 {\n\t\t// Auto-configure local STUN servers for all ports\n\t\tfor _, port := range c.Server.StunPorts {\n\t\t\tc.Management.Stuns = append(c.Management.Stuns, HostConfig{\n\t\t\t\tURI: fmt.Sprintf(\"stun:%s:%d\", exposedHost, port),\n\t\t\t})\n\t\t}\n\t}\n\n\t// Configure relay for clients\n\tif hasExternalRelay {\n\t\t// Use external relay config from server\n\t\tc.Management.Relays = c.Server.Relays\n\t} else if len(c.Management.Relays.Addresses) == 0 {\n\t\t// Auto-configure local relay\n\t\tc.Management.Relays.Addresses = []string{\n\t\t\tfmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort),\n\t\t}\n\t}\n\tif c.Management.Relays.Secret == \"\" {\n\t\tc.Management.Relays.Secret = c.Server.AuthSecret\n\t}\n\tif c.Management.Relays.CredentialsTTL == \"\" {\n\t\tc.Management.Relays.CredentialsTTL = \"12h\"\n\t}\n\n\t// Configure signal for clients\n\tif hasExternalSignal {\n\t\t// Use external signal URI from server config\n\t\tc.Management.SignalURI = c.Server.SignalURI\n\t} else if c.Management.SignalURI == \"\" {\n\t\t// Auto-configure local signal\n\t\tc.Management.SignalURI = fmt.Sprintf(\"%s://%s\", exposedProto, exposedHostPort)\n\t}\n}\n\n// LoadConfig loads configuration from a YAML file\nfunc LoadConfig(configPath string) (*CombinedConfig, error) {\n\tcfg := DefaultConfig()\n\n\tif configPath == \"\" {\n\t\treturn cfg, nil\n\t}\n\n\tdata, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read config file: %w\", err)\n\t}\n\n\tif err := yaml.Unmarshal(data, cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse config file: %w\", err)\n\t}\n\n\t// Populate internal configs from server settings\n\tcfg.ApplySimplifiedDefaults()\n\n\treturn cfg, nil\n}\n\n// Validate validates the configuration\nfunc (c *CombinedConfig) Validate() error {\n\tif c.Server.ExposedAddress == \"\" {\n\t\treturn fmt.Errorf(\"server.exposedAddress is required\")\n\t}\n\tif c.Server.DataDir == \"\" {\n\t\treturn fmt.Errorf(\"server.dataDir is required\")\n\t}\n\n\t// Validate STUN ports\n\tseen := make(map[int]bool)\n\tfor _, port := range c.Server.StunPorts {\n\t\tif port <= 0 || port > 65535 {\n\t\t\treturn fmt.Errorf(\"invalid server.stunPorts value %d: must be between 1 and 65535\", port)\n\t\t}\n\t\tif seen[port] {\n\t\t\treturn fmt.Errorf(\"duplicate STUN port %d in server.stunPorts\", port)\n\t\t}\n\t\tseen[port] = true\n\t}\n\n\t// authSecret is required only if running local relay (no external relay configured)\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\tif !hasExternalRelay && c.Server.AuthSecret == \"\" {\n\t\treturn fmt.Errorf(\"server.authSecret is required when running local relay\")\n\t}\n\n\treturn nil\n}\n\n// HasTLSCert returns true if TLS certificate files are configured\nfunc (c *CombinedConfig) HasTLSCert() bool {\n\treturn c.Server.TLS.CertFile != \"\" && c.Server.TLS.KeyFile != \"\"\n}\n\n// HasLetsEncrypt returns true if Let's Encrypt is configured\nfunc (c *CombinedConfig) HasLetsEncrypt() bool {\n\treturn c.Server.TLS.LetsEncrypt.Enabled &&\n\t\tc.Server.TLS.LetsEncrypt.DataDir != \"\" &&\n\t\tlen(c.Server.TLS.LetsEncrypt.Domains) > 0\n}\n\n// parseExplicitProtocol parses an explicit protocol string to nbconfig.Protocol\nfunc parseExplicitProtocol(proto string) (nbconfig.Protocol, bool) {\n\tswitch strings.ToLower(proto) {\n\tcase \"udp\":\n\t\treturn nbconfig.UDP, true\n\tcase \"dtls\":\n\t\treturn nbconfig.DTLS, true\n\tcase \"tcp\":\n\t\treturn nbconfig.TCP, true\n\tcase \"http\":\n\t\treturn nbconfig.HTTP, true\n\tcase \"https\":\n\t\treturn nbconfig.HTTPS, true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\n// parseStunProtocol determines protocol for STUN/TURN servers.\n// stun: → UDP, stuns: → DTLS, turn: → UDP, turns: → DTLS\n// Explicit proto overrides URI scheme. Defaults to UDP.\nfunc parseStunProtocol(uri, proto string) nbconfig.Protocol {\n\tif proto != \"\" {\n\t\tif p, ok := parseExplicitProtocol(proto); ok {\n\t\t\treturn p\n\t\t}\n\t}\n\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"stuns:\"):\n\t\treturn nbconfig.DTLS\n\tcase strings.HasPrefix(uri, \"turns:\"):\n\t\treturn nbconfig.DTLS\n\tdefault:\n\t\t// stun:, turn:, or no scheme - default to UDP\n\t\treturn nbconfig.UDP\n\t}\n}\n\n// parseSignalProtocol determines protocol for Signal servers.\n// https:// → HTTPS, http:// → HTTP. Defaults to HTTPS.\nfunc parseSignalProtocol(uri string) nbconfig.Protocol {\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"http://\"):\n\t\treturn nbconfig.HTTP\n\tdefault:\n\t\t// https:// or no scheme - default to HTTPS\n\t\treturn nbconfig.HTTPS\n\t}\n}\n\n// stripSignalProtocol removes the protocol prefix from a signal URI.\n// Returns just the host:port (e.g., \"selfhosted2.demo.netbird.io:443\").\nfunc stripSignalProtocol(uri string) string {\n\turi = strings.TrimPrefix(uri, \"https://\")\n\turi = strings.TrimPrefix(uri, \"http://\")\n\treturn uri\n}\n\nfunc buildRelayConfig(relays RelaysConfig) (*nbconfig.Relay, error) {\n\tvar ttl time.Duration\n\tif relays.CredentialsTTL != \"\" {\n\t\tvar err error\n\t\tttl, err = time.ParseDuration(relays.CredentialsTTL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid relay credentials TTL %q: %w\", relays.CredentialsTTL, err)\n\t\t}\n\t}\n\treturn &nbconfig.Relay{\n\t\tAddresses: relays.Addresses,\n\t\tCredentialsTTL: util.Duration{Duration: ttl},\n\t\tSecret: relays.Secret,\n\t}, nil\n}\n\n// buildEmbeddedIdPConfig builds the embedded IdP configuration.\n// authStore overrides auth.storage when set.\nfunc (c *CombinedConfig) buildEmbeddedIdPConfig(mgmt ManagementConfig) (*idp.EmbeddedIdPConfig, error) {\n\tauthStorageType := mgmt.Auth.Storage.Type\n\tauthStorageDSN := c.Server.AuthStore.DSN\n\tif c.Server.AuthStore.Engine != \"\" {\n\t\tauthStorageType = c.Server.AuthStore.Engine\n\t}\n\tif authStorageType == \"\" {\n\t\tauthStorageType = \"sqlite3\"\n\t}\n\tauthStorageFile := \"\"\n\tif authStorageType == \"postgres\" {\n\t\tif authStorageDSN == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"authStore.dsn is required when authStore.engine is postgres\")\n\t\t}\n\t} else {\n\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n\t\tif c.Server.AuthStore.File != \"\" {\n\t\t\tauthStorageFile = c.Server.AuthStore.File\n\t\t}\n\t}\n\n\tcfg := &idp.EmbeddedIdPConfig{\n\t\tEnabled: true,\n\t\tIssuer: mgmt.Auth.Issuer,\n\t\tLocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,\n\t\tSignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,\n\t\tStorage: idp.EmbeddedStorageConfig{\n\t\t\tType: authStorageType,\n\t\t\tConfig: idp.EmbeddedStorageTypeConfig{\n\t\t\t\tFile: authStorageFile,\n\t\t\t\tDSN: authStorageDSN,\n\t\t\t},\n\t\t},\n\t\tDashboardRedirectURIs: mgmt.Auth.DashboardRedirectURIs,\n\t\tCLIRedirectURIs: mgmt.Auth.CLIRedirectURIs,\n\t}\n\n\tif mgmt.Auth.Owner != nil && mgmt.Auth.Owner.Email != \"\" {\n\t\tcfg.Owner = &idp.OwnerConfig{\n\t\t\tEmail: mgmt.Auth.Owner.Email,\n\t\t\tHash: mgmt.Auth.Owner.Password,\n\t\t}\n\t}\n\n\treturn cfg, nil\n}\n\n// ToManagementConfig converts CombinedConfig to management server config\nfunc (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {\n\tmgmt := c.Management\n\n\t// Build STUN hosts\n\tvar stuns []*nbconfig.Host\n\tfor _, s := range mgmt.Stuns {\n\t\tstuns = append(stuns, &nbconfig.Host{\n\t\t\tURI: s.URI,\n\t\t\tProto: parseStunProtocol(s.URI, s.Proto),\n\t\t\tUsername: s.Username,\n\t\t\tPassword: s.Password,\n\t\t})\n\t}\n\n\t// Build relay config\n\tvar relayConfig *nbconfig.Relay\n\tif len(mgmt.Relays.Addresses) > 0 || mgmt.Relays.Secret != \"\" {\n\t\trelay, err := buildRelayConfig(mgmt.Relays)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trelayConfig = relay\n\t}\n\n\t// Build signal config\n\tvar signalConfig *nbconfig.Host\n\tif mgmt.SignalURI != \"\" {\n\t\tsignalConfig = &nbconfig.Host{\n\t\t\tURI: stripSignalProtocol(mgmt.SignalURI),\n\t\t\tProto: parseSignalProtocol(mgmt.SignalURI),\n\t\t}\n\t}\n\n\t// Build store config\n\tstoreConfig := nbconfig.StoreConfig{\n\t\tEngine: types.Engine(mgmt.Store.Engine),\n\t}\n\n\t// Build reverse proxy config\n\treverseProxy := nbconfig.ReverseProxy{\n\t\tTrustedHTTPProxiesCount: mgmt.ReverseProxy.TrustedHTTPProxiesCount,\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedHTTPProxies {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedHTTPProxies = append(reverseProxy.TrustedHTTPProxies, prefix)\n\t\t}\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedPeers {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedPeers = append(reverseProxy.TrustedPeers, prefix)\n\t\t}\n\t}\n\n\t// Build HTTP config (required, even if empty)\n\thttpConfig := &nbconfig.HttpServerConfig{}\n\n\t// Build embedded IDP config (always enabled in combined server)\n\tembeddedIdP, err := c.buildEmbeddedIdPConfig(mgmt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Set HTTP config fields for embedded IDP\n\thttpConfig.AuthIssuer = mgmt.Auth.Issuer\n\thttpConfig.AuthAudience = \"netbird-dashboard\"\n\thttpConfig.AuthClientID = httpConfig.AuthAudience\n\thttpConfig.CLIAuthAudience = \"netbird-cli\"\n\thttpConfig.AuthUserIDClaim = \"sub\"\n\thttpConfig.AuthKeysLocation = mgmt.Auth.Issuer + \"/keys\"\n\thttpConfig.OIDCConfigEndpoint = mgmt.Auth.Issuer + \"/.well-known/openid-configuration\"\n\thttpConfig.IdpSignKeyRefreshEnabled = mgmt.Auth.SignKeyRefreshEnabled\n\tcallbackURL := strings.TrimSuffix(httpConfig.AuthIssuer, \"/oauth2\")\n\thttpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull\n\n\treturn &nbconfig.Config{\n\t\tStuns: stuns,\n\t\tRelay: relayConfig,\n\t\tSignal: signalConfig,\n\t\tDatadir: mgmt.DataDir,\n\t\tDataStoreEncryptionKey: mgmt.Store.EncryptionKey,\n\t\tHttpConfig: httpConfig,\n\t\tStoreConfig: storeConfig,\n\t\tReverseProxy: reverseProxy,\n\t\tDisableDefaultPolicy: mgmt.DisableDefaultPolicy,\n\t\tEmbeddedIdP: embeddedIdP,\n\t}, nil\n}\n\n// ApplyEmbeddedIdPConfig applies embedded IdP configuration to the management config.\n// This mirrors the logic in management/cmd/management.go ApplyEmbeddedIdPConfig.\nfunc ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config, mgmtPort int, disableSingleAccMode bool) error {\n\tif cfg.EmbeddedIdP == nil || !cfg.EmbeddedIdP.Enabled {\n\t\treturn nil\n\t}\n\n\t// Embedded IdP requires single account mode\n\tif disableSingleAccMode {\n\t\treturn fmt.Errorf(\"embedded IdP requires single account mode; multiple account mode is not supported with embedded IdP\")\n\t}\n\n\t// Set LocalAddress for embedded IdP, used for internal JWT validation\n\tcfg.EmbeddedIdP.LocalAddress = fmt.Sprintf(\"localhost:%d\", mgmtPort)\n\n\t// Set storage defaults based on Datadir\n\tif cfg.EmbeddedIdP.Storage.Type == \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Type = \"sqlite3\"\n\t}\n\tif cfg.EmbeddedIdP.Storage.Config.File == \"\" && cfg.Datadir != \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")\n\t}\n\n\tissuer := cfg.EmbeddedIdP.Issuer\n\n\t// Ensure HttpConfig exists\n\tif cfg.HttpConfig == nil {\n\t\tcfg.HttpConfig = &nbconfig.HttpServerConfig{}\n\t}\n\n\t// Set HttpConfig values from EmbeddedIdP\n\tcfg.HttpConfig.AuthIssuer = issuer\n\tcfg.HttpConfig.AuthAudience = \"netbird-dashboard\"\n\tcfg.HttpConfig.CLIAuthAudience = \"netbird-cli\"\n\tcfg.HttpConfig.AuthUserIDClaim = \"sub\"\n\tcfg.HttpConfig.AuthKeysLocation = issuer + \"/keys\"\n\tcfg.HttpConfig.OIDCConfigEndpoint = issuer + \"/.well-known/openid-configuration\"\n\tcfg.HttpConfig.IdpSignKeyRefreshEnabled = true\n\n\treturn nil\n}\n\n// EnsureEncryptionKey generates an encryption key if not set.\n// Unlike management server, we don't write back to the config file.\nfunc EnsureEncryptionKey(ctx context.Context, cfg *nbconfig.Config) error {\n\tif cfg.DataStoreEncryptionKey != \"\" {\n\t\treturn nil\n\t}\n\n\tlog.WithContext(ctx).Infof(\"DataStoreEncryptionKey is not set, generating a new key\")\n\tkey, err := crypt.GenerateKey()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate datastore encryption key: %v\", err)\n\t}\n\tcfg.DataStoreEncryptionKey = key\n\tkeyPreview := key[:8] + \"...\"\n\tlog.WithContext(ctx).Warnf(\"DataStoreEncryptionKey generated (%s); add it to your config file under 'server.store.encryptionKey' to persist across restarts\", keyPreview)\n\n\treturn nil\n}\n\n// LogConfigInfo logs informational messages about the loaded configuration\nfunc LogConfigInfo(cfg *nbconfig.Config) {\n\tif cfg.EmbeddedIdP != nil && cfg.EmbeddedIdP.Enabled {\n\t\tlog.Infof(\"running with the embedded IdP: %v\", cfg.EmbeddedIdP.Issuer)\n\t}\n\tif cfg.Relay != nil {\n\t\tlog.Infof(\"Relay addresses: %v\", cfg.Relay.Addresses)\n\t}\n}","newString":"package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/netbirdio/netbird/management/server/idp\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n\n\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n)\n\n// CombinedConfig is the root configuration for the combined server.\n// The combined server is primarily a Management server with optional embedded\n// Signal, Relay, and STUN services.\n//\n// Architecture:\n// - Management: Always runs locally (this IS the management server)\n// - Signal: Runs locally by default; disabled if server.signalUri is set\n// - Relay: Runs locally by default; disabled if server.relays is set\n// - STUN: Runs locally on port 3478 by default; disabled if server.stuns is set\n//\n// All user-facing settings are under \"server\". The relay/signal/management\n// fields are internal and populated automatically from server settings.\ntype CombinedConfig struct {\n\tServer ServerConfig `yaml:\"server\"`\n\n\t// Internal configs - populated from Server settings, not user-configurable\n\tRelay RelayConfig `yaml:\"-\"`\n\tSignal SignalConfig `yaml:\"-\"`\n\tManagement ManagementConfig `yaml:\"-\"`\n}\n\n// ServerConfig contains server-wide settings\n// In simplified mode, this contains all configuration\ntype ServerConfig struct {\n\tListenAddress string `yaml:\"listenAddress\"`\n\tMetricsPort int `yaml:\"metricsPort\"`\n\tHealthcheckAddress string `yaml:\"healthcheckAddress\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tLogFile string `yaml:\"logFile\"`\n\tTLS TLSConfig `yaml:\"tls\"`\n\n\t// Simplified config fields (used when relay/signal/management sections are omitted)\n\tExposedAddress string `yaml:\"exposedAddress\"` // Public address with protocol (e.g., \"https://example.com:443\")\n\tStunPorts []int `yaml:\"stunPorts\"` // STUN ports (empty to disable local STUN)\n\tAuthSecret string `yaml:\"authSecret\"` // Shared secret for relay authentication\n\tDataDir string `yaml:\"dataDir\"` // Data directory for all services\n\n\t// External service overrides (simplified mode)\n\t// When these are set, the corresponding local service is NOT started\n\t// and these values are used for client configuration instead\n\tStuns []HostConfig `yaml:\"stuns\"` // External STUN servers (disables local STUN)\n\tRelays RelaysConfig `yaml:\"relays\"` // External relay servers (disables local relay)\n\tSignalURI string `yaml:\"signalUri\"` // External signal server (disables local signal)\n\n\t// Management settings (simplified mode)\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tActivityStore StoreConfig `yaml:\"activityStore\"`\n\tAuthStore StoreConfig `yaml:\"authStore\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// TLSConfig contains TLS/HTTPS settings\ntype TLSConfig struct {\n\tCertFile string `yaml:\"certFile\"`\n\tKeyFile string `yaml:\"keyFile\"`\n\tLetsEncrypt LetsEncryptConfig `yaml:\"letsencrypt\"`\n}\n\n// LetsEncryptConfig contains Let's Encrypt settings\ntype LetsEncryptConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDomains []string `yaml:\"domains\"`\n\tEmail string `yaml:\"email\"`\n\tAWSRoute53 bool `yaml:\"awsRoute53\"`\n}\n\n// RelayConfig contains relay service settings\ntype RelayConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tExposedAddress string `yaml:\"exposedAddress\"`\n\tAuthSecret string `yaml:\"authSecret\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tStun StunConfig `yaml:\"stun\"`\n}\n\n// StunConfig contains embedded STUN service settings\ntype StunConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tPorts []int `yaml:\"ports\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// SignalConfig contains signal service settings\ntype SignalConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// ManagementConfig contains management service settings\ntype ManagementConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDnsDomain string `yaml:\"dnsDomain\"`\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tDisableDefaultPolicy bool `yaml:\"disableDefaultPolicy\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStuns []HostConfig `yaml:\"stuns\"`\n\tRelays RelaysConfig `yaml:\"relays\"`\n\tSignalURI string `yaml:\"signalUri\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// AuthConfig contains authentication/identity provider settings\ntype AuthConfig struct {\n\tIssuer string `yaml:\"issuer\"`\n\tLocalAuthDisabled bool `yaml:\"localAuthDisabled\"`\n\tSignKeyRefreshEnabled bool `yaml:\"signKeyRefreshEnabled\"`\n\tStorage AuthStorageConfig `yaml:\"storage\"`\n\tDashboardRedirectURIs []string `yaml:\"dashboardRedirectURIs\"`\n\tCLIRedirectURIs []string `yaml:\"cliRedirectURIs\"`\n\tOwner *AuthOwnerConfig `yaml:\"owner,omitempty\"`\n}\n\n// AuthStorageConfig contains auth storage settings\ntype AuthStorageConfig struct {\n\tType string `yaml:\"type\"`\n\tFile string `yaml:\"file\"`\n}\n\n// AuthOwnerConfig contains initial admin user settings\ntype AuthOwnerConfig struct {\n\tEmail string `yaml:\"email\"`\n\tPassword string `yaml:\"password\"`\n}\n\n// HostConfig represents a STUN/TURN/Signal host\ntype HostConfig struct {\n\tURI string `yaml:\"uri\"`\n\tProto string `yaml:\"proto,omitempty\"` // udp, dtls, tcp, http, https - defaults based on URI scheme\n\tUsername string `yaml:\"username,omitempty\"`\n\tPassword string `yaml:\"password,omitempty\"`\n}\n\n// RelaysConfig contains external relay server settings for clients\ntype RelaysConfig struct {\n\tAddresses []string `yaml:\"addresses\"`\n\tCredentialsTTL string `yaml:\"credentialsTTL\"`\n\tSecret string `yaml:\"secret\"`\n}\n\n// StoreConfig contains database settings\ntype StoreConfig struct {\n\tEngine string `yaml:\"engine\"`\n\tEncryptionKey string `yaml:\"encryptionKey\"`\n\tDSN string `yaml:\"dsn\"` // Connection string for postgres or mysql engines\n\tFile string `yaml:\"file\"` // SQLite database file path (optional, defaults to dataDir)\n}\n\n// ReverseProxyConfig contains reverse proxy settings\ntype ReverseProxyConfig struct {\n\tTrustedHTTPProxies []string `yaml:\"trustedHTTPProxies\"`\n\tTrustedHTTPProxiesCount uint `yaml:\"trustedHTTPProxiesCount\"`\n\tTrustedPeers []string `yaml:\"trustedPeers\"`\n}\n\n// DefaultConfig returns a CombinedConfig with default values\nfunc DefaultConfig() *CombinedConfig {\n\treturn &CombinedConfig{\n\t\tServer: ServerConfig{\n\t\t\tListenAddress: \":443\",\n\t\t\tMetricsPort: 9090,\n\t\t\tHealthcheckAddress: \":9000\",\n\t\t\tLogLevel: \"info\",\n\t\t\tLogFile: \"console\",\n\t\t\tStunPorts: []int{3478},\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t\tRelay: RelayConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\tStun: StunConfig{\n\t\t\t\tEnabled: false,\n\t\t\t\tPorts: []int{3478},\n\t\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\t},\n\t\t},\n\t\tSignal: SignalConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t},\n\t\tManagement: ManagementConfig{\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRelays: RelaysConfig{\n\t\t\t\tCredentialsTTL: \"12h\",\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t}\n}\n\n// hasRequiredSettings returns true if the configuration has the required server settings\nfunc (c *CombinedConfig) hasRequiredSettings() bool {\n\treturn c.Server.ExposedAddress != \"\"\n}\n\n// parseExposedAddress extracts protocol, host, and host:port from the exposed address\n// Input format: \"https://example.com:443\" or \"http://example.com:8080\" or \"example.com:443\"\n// Returns: protocol (\"https\" or \"http\"), hostname only, and host:port\nfunc parseExposedAddress(exposedAddress string) (protocol, hostname, hostPort string) {\n\t// Default to https if no protocol specified\n\tprotocol = \"https\"\n\thostPort = exposedAddress\n\n\t// Check for protocol prefix\n\tif strings.HasPrefix(exposedAddress, \"https://\") {\n\t\tprotocol = \"https\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"https://\")\n\t} else if strings.HasPrefix(exposedAddress, \"http://\") {\n\t\tprotocol = \"http\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"http://\")\n\t}\n\n\t// Extract hostname (without port)\n\thostname = hostPort\n\tif host, _, err := net.SplitHostPort(hostPort); err == nil {\n\t\thostname = host\n\t}\n\n\treturn protocol, hostname, hostPort\n}\n\n// ApplySimplifiedDefaults populates internal relay/signal/management configs from server settings.\n// Management is always enabled. Signal, Relay, and STUN are enabled unless external\n// overrides are configured (server.signalUri, server.relays, server.stuns).\nfunc (c *CombinedConfig) ApplySimplifiedDefaults() {\n\tif !c.hasRequiredSettings() {\n\t\treturn\n\t}\n\n\t// Parse exposed address to extract protocol and hostname\n\texposedProto, exposedHost, exposedHostPort := parseExposedAddress(c.Server.ExposedAddress)\n\n\t// Check for external service overrides\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\thasExternalSignal := c.Server.SignalURI != \"\"\n\thasExternalStuns := len(c.Server.Stuns) > 0\n\n\t// Default stunPorts to [3478] if not specified and no external STUN\n\tif len(c.Server.StunPorts) == 0 && !hasExternalStuns {\n\t\tc.Server.StunPorts = []int{3478}\n\t}\n\n\tc.applyRelayDefaults(exposedProto, exposedHostPort, hasExternalRelay, hasExternalStuns)\n\tc.applySignalDefaults(hasExternalSignal)\n\tc.applyManagementDefaults(exposedHost)\n\n\t// Auto-configure client settings (stuns, relays, signalUri)\n\tc.autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort, hasExternalStuns, hasExternalRelay, hasExternalSignal)\n}\n\n// applyRelayDefaults configures the relay service if no external relay is configured.\nfunc (c *CombinedConfig) applyRelayDefaults(exposedProto, exposedHostPort string, hasExternalRelay, hasExternalStuns bool) {\n\tif hasExternalRelay {\n\t\treturn\n\t}\n\n\tc.Relay.Enabled = true\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\tc.Relay.ExposedAddress = fmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort)\n\tc.Relay.AuthSecret = c.Server.AuthSecret\n\tif c.Relay.LogLevel == \"\" {\n\t\tc.Relay.LogLevel = c.Server.LogLevel\n\t}\n\n\t// Enable local STUN only if no external STUN servers and stunPorts are configured\n\tif !hasExternalStuns && len(c.Server.StunPorts) > 0 {\n\t\tc.Relay.Stun.Enabled = true\n\t\tc.Relay.Stun.Ports = c.Server.StunPorts\n\t\tif c.Relay.Stun.LogLevel == \"\" {\n\t\t\tc.Relay.Stun.LogLevel = c.Server.LogLevel\n\t\t}\n\t}\n}\n\n// applySignalDefaults configures the signal service if no external signal is configured.\nfunc (c *CombinedConfig) applySignalDefaults(hasExternalSignal bool) {\n\tif hasExternalSignal {\n\t\treturn\n\t}\n\n\tc.Signal.Enabled = true\n\tif c.Signal.LogLevel == \"\" {\n\t\tc.Signal.LogLevel = c.Server.LogLevel\n\t}\n}\n\n// applyManagementDefaults configures the management service (always enabled).\nfunc (c *CombinedConfig) applyManagementDefaults(exposedHost string) {\n\tc.Management.Enabled = true\n\tif c.Management.LogLevel == \"\" {\n\t\tc.Management.LogLevel = c.Server.LogLevel\n\t}\n\tif c.Management.DataDir == \"\" || c.Management.DataDir == \"/var/lib/netbird/\" {\n\t\tc.Management.DataDir = c.Server.DataDir\n\t}\n\tc.Management.DnsDomain = exposedHost\n\tc.Management.DisableAnonymousMetrics = c.Server.DisableAnonymousMetrics\n\tc.Management.DisableGeoliteUpdate = c.Server.DisableGeoliteUpdate\n\t// Copy auth config from server if management auth issuer is not set\n\tif c.Management.Auth.Issuer == \"\" && c.Server.Auth.Issuer != \"\" {\n\t\tc.Management.Auth = c.Server.Auth\n\t}\n\n\t// Copy store config from server if not set\n\tif c.Management.Store.Engine == \"\" || c.Management.Store.Engine == \"sqlite\" {\n\t\tif c.Server.Store.Engine != \"\" {\n\t\t\tc.Management.Store = c.Server.Store\n\t\t}\n\t}\n\n\t// Copy reverse proxy config from server\n\tif len(c.Server.ReverseProxy.TrustedHTTPProxies) > 0 || c.Server.ReverseProxy.TrustedHTTPProxiesCount > 0 || len(c.Server.ReverseProxy.TrustedPeers) > 0 {\n\t\tc.Management.ReverseProxy = c.Server.ReverseProxy\n\t}\n}\n\n// autoConfigureClientSettings sets up STUN/relay/signal URIs for clients\n// External overrides from server config take precedence over auto-generated values\nfunc (c *CombinedConfig) autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort string, hasExternalStuns, hasExternalRelay, hasExternalSignal bool) {\n\t// Determine relay protocol from exposed protocol\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\n\t// Configure STUN servers for clients\n\tif hasExternalStuns {\n\t\t// Use external STUN servers from server config\n\t\tc.Management.Stuns = c.Server.Stuns\n\t} else if len(c.Server.StunPorts) > 0 && len(c.Management.Stuns) == 0 {\n\t\t// Auto-configure local STUN servers for all ports\n\t\tfor _, port := range c.Server.StunPorts {\n\t\t\tc.Management.Stuns = append(c.Management.Stuns, HostConfig{\n\t\t\t\tURI: fmt.Sprintf(\"stun:%s:%d\", exposedHost, port),\n\t\t\t})\n\t\t}\n\t}\n\n\t// Configure relay for clients\n\tif hasExternalRelay {\n\t\t// Use external relay config from server\n\t\tc.Management.Relays = c.Server.Relays\n\t} else if len(c.Management.Relays.Addresses) == 0 {\n\t\t// Auto-configure local relay\n\t\tc.Management.Relays.Addresses = []string{\n\t\t\tfmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort),\n\t\t}\n\t}\n\tif c.Management.Relays.Secret == \"\" {\n\t\tc.Management.Relays.Secret = c.Server.AuthSecret\n\t}\n\tif c.Management.Relays.CredentialsTTL == \"\" {\n\t\tc.Management.Relays.CredentialsTTL = \"12h\"\n\t}\n\n\t// Configure signal for clients\n\tif hasExternalSignal {\n\t\t// Use external signal URI from server config\n\t\tc.Management.SignalURI = c.Server.SignalURI\n\t} else if c.Management.SignalURI == \"\" {\n\t\t// Auto-configure local signal\n\t\tc.Management.SignalURI = fmt.Sprintf(\"%s://%s\", exposedProto, exposedHostPort)\n\t}\n}\n\n// LoadConfig loads configuration from a YAML file\nfunc LoadConfig(configPath string) (*CombinedConfig, error) {\n\tcfg := DefaultConfig()\n\n\tif configPath == \"\" {\n\t\treturn cfg, nil\n\t}\n\n\tdata, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read config file: %w\", err)\n\t}\n\n\tif err := yaml.Unmarshal(data, cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse config file: %w\", err)\n\t}\n\n\t// Populate internal configs from server settings\n\tcfg.ApplySimplifiedDefaults()\n\n\treturn cfg, nil\n}\n\n// Validate validates the configuration\nfunc (c *CombinedConfig) Validate() error {\n\tif c.Server.ExposedAddress == \"\" {\n\t\treturn fmt.Errorf(\"server.exposedAddress is required\")\n\t}\n\tif c.Server.DataDir == \"\" {\n\t\treturn fmt.Errorf(\"server.dataDir is required\")\n\t}\n\n\t// Validate STUN ports\n\tseen := make(map[int]bool)\n\tfor _, port := range c.Server.StunPorts {\n\t\tif port <= 0 || port > 65535 {\n\t\t\treturn fmt.Errorf(\"invalid server.stunPorts value %d: must be between 1 and 65535\", port)\n\t\t}\n\t\tif seen[port] {\n\t\t\treturn fmt.Errorf(\"duplicate STUN port %d in server.stunPorts\", port)\n\t\t}\n\t\tseen[port] = true\n\t}\n\n\t// authSecret is required only if running local relay (no external relay configured)\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\tif !hasExternalRelay && c.Server.AuthSecret == \"\" {\n\t\treturn fmt.Errorf(\"server.authSecret is required when running local relay\")\n\t}\n\n\treturn nil\n}\n\n// HasTLSCert returns true if TLS certificate files are configured\nfunc (c *CombinedConfig) HasTLSCert() bool {\n\treturn c.Server.TLS.CertFile != \"\" && c.Server.TLS.KeyFile != \"\"\n}\n\n// HasLetsEncrypt returns true if Let's Encrypt is configured\nfunc (c *CombinedConfig) HasLetsEncrypt() bool {\n\treturn c.Server.TLS.LetsEncrypt.Enabled &&\n\t\tc.Server.TLS.LetsEncrypt.DataDir != \"\" &&\n\t\tlen(c.Server.TLS.LetsEncrypt.Domains) > 0\n}\n\n// parseExplicitProtocol parses an explicit protocol string to nbconfig.Protocol\nfunc parseExplicitProtocol(proto string) (nbconfig.Protocol, bool) {\n\tswitch strings.ToLower(proto) {\n\tcase \"udp\":\n\t\treturn nbconfig.UDP, true\n\tcase \"dtls\":\n\t\treturn nbconfig.DTLS, true\n\tcase \"tcp\":\n\t\treturn nbconfig.TCP, true\n\tcase \"http\":\n\t\treturn nbconfig.HTTP, true\n\tcase \"https\":\n\t\treturn nbconfig.HTTPS, true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\n// parseStunProtocol determines protocol for STUN/TURN servers.\n// stun: → UDP, stuns: → DTLS, turn: → UDP, turns: → DTLS\n// Explicit proto overrides URI scheme. Defaults to UDP.\nfunc parseStunProtocol(uri, proto string) nbconfig.Protocol {\n\tif proto != \"\" {\n\t\tif p, ok := parseExplicitProtocol(proto); ok {\n\t\t\treturn p\n\t\t}\n\t}\n\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"stuns:\"):\n\t\treturn nbconfig.DTLS\n\tcase strings.HasPrefix(uri, \"turns:\"):\n\t\treturn nbconfig.DTLS\n\tdefault:\n\t\t// stun:, turn:, or no scheme - default to UDP\n\t\treturn nbconfig.UDP\n\t}\n}\n\n// parseSignalProtocol determines protocol for Signal servers.\n// https:// → HTTPS, http:// → HTTP. Defaults to HTTPS.\nfunc parseSignalProtocol(uri string) nbconfig.Protocol {\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"http://\"):\n\t\treturn nbconfig.HTTP\n\tdefault:\n\t\t// https:// or no scheme - default to HTTPS\n\t\treturn nbconfig.HTTPS\n\t}\n}\n\n// stripSignalProtocol removes the protocol prefix from a signal URI.\n// Returns just the host:port (e.g., \"selfhosted2.demo.netbird.io:443\").\nfunc stripSignalProtocol(uri string) string {\n\turi = strings.TrimPrefix(uri, \"https://\")\n\turi = strings.TrimPrefix(uri, \"http://\")\n\treturn uri\n}\n\nfunc buildRelayConfig(relays RelaysConfig) (*nbconfig.Relay, error) {\n\tvar ttl time.Duration\n\tif relays.CredentialsTTL != \"\" {\n\t\tvar err error\n\t\tttl, err = time.ParseDuration(relays.CredentialsTTL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid relay credentials TTL %q: %w\", relays.CredentialsTTL, err)\n\t\t}\n\t}\n\treturn &nbconfig.Relay{\n\t\tAddresses: relays.Addresses,\n\t\tCredentialsTTL: util.Duration{Duration: ttl},\n\t\tSecret: relays.Secret,\n\t}, nil\n}\n\n// buildEmbeddedIdPConfig builds the embedded IdP configuration.\n// authStore overrides auth.storage when set.\nfunc (c *CombinedConfig) buildEmbeddedIdPConfig(mgmt ManagementConfig) (*idp.EmbeddedIdPConfig, error) {\n\tauthStorageType := mgmt.Auth.Storage.Type\n\tauthStorageDSN := c.Server.AuthStore.DSN\n\tif c.Server.AuthStore.Engine != \"\" {\n\t\tauthStorageType = c.Server.AuthStore.Engine\n\t}\n\tif authStorageType == \"\" {\n\t\tauthStorageType = \"sqlite3\"\n\t}\n\tauthStorageFile := \"\"\n\tif authStorageType == \"postgres\" {\n\t\tif authStorageDSN == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"authStore.dsn is required when authStore.engine is postgres\")\n\t\t}\n\t} else {\n\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n\t\tif c.Server.AuthStore.File != \"\" {\n\t\t\tauthStorageFile = c.Server.AuthStore.File\n\t\t\tif !filepath.IsAbs(authStorageFile) {\n\t\t\t\tauthStorageFile = filepath.Join(mgmt.DataDir, authStorageFile)\n\t\t\t}\n\t\t}\n\t}\n\n\tcfg := &idp.EmbeddedIdPConfig{\n\t\tEnabled: true,\n\t\tIssuer: mgmt.Auth.Issuer,\n\t\tLocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,\n\t\tSignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,\n\t\tStorage: idp.EmbeddedStorageConfig{\n\t\t\tType: authStorageType,\n\t\t\tConfig: idp.EmbeddedStorageTypeConfig{\n\t\t\t\tFile: authStorageFile,\n\t\t\t\tDSN: authStorageDSN,\n\t\t\t},\n\t\t},\n\t\tDashboardRedirectURIs: mgmt.Auth.DashboardRedirectURIs,\n\t\tCLIRedirectURIs: mgmt.Auth.CLIRedirectURIs,\n\t}\n\n\tif mgmt.Auth.Owner != nil && mgmt.Auth.Owner.Email != \"\" {\n\t\tcfg.Owner = &idp.OwnerConfig{\n\t\t\tEmail: mgmt.Auth.Owner.Email,\n\t\t\tHash: mgmt.Auth.Owner.Password,\n\t\t}\n\t}\n\n\treturn cfg, nil\n}\n\n// ToManagementConfig converts CombinedConfig to management server config\nfunc (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {\n\tmgmt := c.Management\n\n\t// Build STUN hosts\n\tvar stuns []*nbconfig.Host\n\tfor _, s := range mgmt.Stuns {\n\t\tstuns = append(stuns, &nbconfig.Host{\n\t\t\tURI: s.URI,\n\t\t\tProto: parseStunProtocol(s.URI, s.Proto),\n\t\t\tUsername: s.Username,\n\t\t\tPassword: s.Password,\n\t\t})\n\t}\n\n\t// Build relay config\n\tvar relayConfig *nbconfig.Relay\n\tif len(mgmt.Relays.Addresses) > 0 || mgmt.Relays.Secret != \"\" {\n\t\trelay, err := buildRelayConfig(mgmt.Relays)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trelayConfig = relay\n\t}\n\n\t// Build signal config\n\tvar signalConfig *nbconfig.Host\n\tif mgmt.SignalURI != \"\" {\n\t\tsignalConfig = &nbconfig.Host{\n\t\t\tURI: stripSignalProtocol(mgmt.SignalURI),\n\t\t\tProto: parseSignalProtocol(mgmt.SignalURI),\n\t\t}\n\t}\n\n\t// Build store config\n\tstoreConfig := nbconfig.StoreConfig{\n\t\tEngine: types.Engine(mgmt.Store.Engine),\n\t}\n\n\t// Build reverse proxy config\n\treverseProxy := nbconfig.ReverseProxy{\n\t\tTrustedHTTPProxiesCount: mgmt.ReverseProxy.TrustedHTTPProxiesCount,\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedHTTPProxies {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedHTTPProxies = append(reverseProxy.TrustedHTTPProxies, prefix)\n\t\t}\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedPeers {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedPeers = append(reverseProxy.TrustedPeers, prefix)\n\t\t}\n\t}\n\n\t// Build HTTP config (required, even if empty)\n\thttpConfig := &nbconfig.HttpServerConfig{}\n\n\t// Build embedded IDP config (always enabled in combined server)\n\tembeddedIdP, err := c.buildEmbeddedIdPConfig(mgmt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Set HTTP config fields for embedded IDP\n\thttpConfig.AuthIssuer = mgmt.Auth.Issuer\n\thttpConfig.AuthAudience = \"netbird-dashboard\"\n\thttpConfig.AuthClientID = httpConfig.AuthAudience\n\thttpConfig.CLIAuthAudience = \"netbird-cli\"\n\thttpConfig.AuthUserIDClaim = \"sub\"\n\thttpConfig.AuthKeysLocation = mgmt.Auth.Issuer + \"/keys\"\n\thttpConfig.OIDCConfigEndpoint = mgmt.Auth.Issuer + \"/.well-known/openid-configuration\"\n\thttpConfig.IdpSignKeyRefreshEnabled = mgmt.Auth.SignKeyRefreshEnabled\n\tcallbackURL := strings.TrimSuffix(httpConfig.AuthIssuer, \"/oauth2\")\n\thttpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull\n\n\treturn &nbconfig.Config{\n\t\tStuns: stuns,\n\t\tRelay: relayConfig,\n\t\tSignal: signalConfig,\n\t\tDatadir: mgmt.DataDir,\n\t\tDataStoreEncryptionKey: mgmt.Store.EncryptionKey,\n\t\tHttpConfig: httpConfig,\n\t\tStoreConfig: storeConfig,\n\t\tReverseProxy: reverseProxy,\n\t\tDisableDefaultPolicy: mgmt.DisableDefaultPolicy,\n\t\tEmbeddedIdP: embeddedIdP,\n\t}, nil\n}\n\n// ApplyEmbeddedIdPConfig applies embedded IdP configuration to the management config.\n// This mirrors the logic in management/cmd/management.go ApplyEmbeddedIdPConfig.\nfunc ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config, mgmtPort int, disableSingleAccMode bool) error {\n\tif cfg.EmbeddedIdP == nil || !cfg.EmbeddedIdP.Enabled {\n\t\treturn nil\n\t}\n\n\t// Embedded IdP requires single account mode\n\tif disableSingleAccMode {\n\t\treturn fmt.Errorf(\"embedded IdP requires single account mode; multiple account mode is not supported with embedded IdP\")\n\t}\n\n\t// Set LocalAddress for embedded IdP, used for internal JWT validation\n\tcfg.EmbeddedIdP.LocalAddress = fmt.Sprintf(\"localhost:%d\", mgmtPort)\n\n\t// Set storage defaults based on Datadir\n\tif cfg.EmbeddedIdP.Storage.Type == \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Type = \"sqlite3\"\n\t}\n\tif cfg.EmbeddedIdP.Storage.Config.File == \"\" && cfg.Datadir != \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")\n\t}\n\n\tissuer := cfg.EmbeddedIdP.Issuer\n\n\t// Ensure HttpConfig exists\n\tif cfg.HttpConfig == nil {\n\t\tcfg.HttpConfig = &nbconfig.HttpServerConfig{}\n\t}\n\n\t// Set HttpConfig values from EmbeddedIdP\n\tcfg.HttpConfig.AuthIssuer = issuer\n\tcfg.HttpConfig.AuthAudience = \"netbird-dashboard\"\n\tcfg.HttpConfig.CLIAuthAudience = \"netbird-cli\"\n\tcfg.HttpConfig.AuthUserIDClaim = \"sub\"\n\tcfg.HttpConfig.AuthKeysLocation = issuer + \"/keys\"\n\tcfg.HttpConfig.OIDCConfigEndpoint = issuer + \"/.well-known/openid-configuration\"\n\tcfg.HttpConfig.IdpSignKeyRefreshEnabled = true\n\n\treturn nil\n}\n\n// EnsureEncryptionKey generates an encryption key if not set.\n// Unlike management server, we don't write back to the config file.\nfunc EnsureEncryptionKey(ctx context.Context, cfg *nbconfig.Config) error {\n\tif cfg.DataStoreEncryptionKey != \"\" {\n\t\treturn nil\n\t}\n\n\tlog.WithContext(ctx).Infof(\"DataStoreEncryptionKey is not set, generating a new key\")\n\tkey, err := crypt.GenerateKey()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate datastore encryption key: %v\", err)\n\t}\n\tcfg.DataStoreEncryptionKey = key\n\tkeyPreview := key[:8] + \"...\"\n\tlog.WithContext(ctx).Warnf(\"DataStoreEncryptionKey generated (%s); add it to your config file under 'server.store.encryptionKey' to persist across restarts\", keyPreview)\n\n\treturn nil\n}\n\n// LogConfigInfo logs informational messages about the loaded configuration\nfunc LogConfigInfo(cfg *nbconfig.Config) {\n\tif cfg.EmbeddedIdP != nil && cfg.EmbeddedIdP.Enabled {\n\t\tlog.Infof(\"running with the embedded IdP: %v\", cfg.EmbeddedIdP.Issuer)\n\t}\n\tif cfg.Relay != nil {\n\t\tlog.Infof(\"Relay addresses: %v\", cfg.Relay.Addresses)\n\t}\n}","originalFile":"package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/netbirdio/netbird/management/server/idp\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/util\"\n\t\"github.com/netbirdio/netbird/util/crypt\"\n\n\tnbconfig \"github.com/netbirdio/netbird/management/internals/server/config\"\n)\n\n// CombinedConfig is the root configuration for the combined server.\n// The combined server is primarily a Management server with optional embedded\n// Signal, Relay, and STUN services.\n//\n// Architecture:\n// - Management: Always runs locally (this IS the management server)\n// - Signal: Runs locally by default; disabled if server.signalUri is set\n// - Relay: Runs locally by default; disabled if server.relays is set\n// - STUN: Runs locally on port 3478 by default; disabled if server.stuns is set\n//\n// All user-facing settings are under \"server\". The relay/signal/management\n// fields are internal and populated automatically from server settings.\ntype CombinedConfig struct {\n\tServer ServerConfig `yaml:\"server\"`\n\n\t// Internal configs - populated from Server settings, not user-configurable\n\tRelay RelayConfig `yaml:\"-\"`\n\tSignal SignalConfig `yaml:\"-\"`\n\tManagement ManagementConfig `yaml:\"-\"`\n}\n\n// ServerConfig contains server-wide settings\n// In simplified mode, this contains all configuration\ntype ServerConfig struct {\n\tListenAddress string `yaml:\"listenAddress\"`\n\tMetricsPort int `yaml:\"metricsPort\"`\n\tHealthcheckAddress string `yaml:\"healthcheckAddress\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tLogFile string `yaml:\"logFile\"`\n\tTLS TLSConfig `yaml:\"tls\"`\n\n\t// Simplified config fields (used when relay/signal/management sections are omitted)\n\tExposedAddress string `yaml:\"exposedAddress\"` // Public address with protocol (e.g., \"https://example.com:443\")\n\tStunPorts []int `yaml:\"stunPorts\"` // STUN ports (empty to disable local STUN)\n\tAuthSecret string `yaml:\"authSecret\"` // Shared secret for relay authentication\n\tDataDir string `yaml:\"dataDir\"` // Data directory for all services\n\n\t// External service overrides (simplified mode)\n\t// When these are set, the corresponding local service is NOT started\n\t// and these values are used for client configuration instead\n\tStuns []HostConfig `yaml:\"stuns\"` // External STUN servers (disables local STUN)\n\tRelays RelaysConfig `yaml:\"relays\"` // External relay servers (disables local relay)\n\tSignalURI string `yaml:\"signalUri\"` // External signal server (disables local signal)\n\n\t// Management settings (simplified mode)\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tActivityStore StoreConfig `yaml:\"activityStore\"`\n\tAuthStore StoreConfig `yaml:\"authStore\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// TLSConfig contains TLS/HTTPS settings\ntype TLSConfig struct {\n\tCertFile string `yaml:\"certFile\"`\n\tKeyFile string `yaml:\"keyFile\"`\n\tLetsEncrypt LetsEncryptConfig `yaml:\"letsencrypt\"`\n}\n\n// LetsEncryptConfig contains Let's Encrypt settings\ntype LetsEncryptConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDomains []string `yaml:\"domains\"`\n\tEmail string `yaml:\"email\"`\n\tAWSRoute53 bool `yaml:\"awsRoute53\"`\n}\n\n// RelayConfig contains relay service settings\ntype RelayConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tExposedAddress string `yaml:\"exposedAddress\"`\n\tAuthSecret string `yaml:\"authSecret\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tStun StunConfig `yaml:\"stun\"`\n}\n\n// StunConfig contains embedded STUN service settings\ntype StunConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tPorts []int `yaml:\"ports\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// SignalConfig contains signal service settings\ntype SignalConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n}\n\n// ManagementConfig contains management service settings\ntype ManagementConfig struct {\n\tEnabled bool `yaml:\"enabled\"`\n\tLogLevel string `yaml:\"logLevel\"`\n\tDataDir string `yaml:\"dataDir\"`\n\tDnsDomain string `yaml:\"dnsDomain\"`\n\tDisableAnonymousMetrics bool `yaml:\"disableAnonymousMetrics\"`\n\tDisableGeoliteUpdate bool `yaml:\"disableGeoliteUpdate\"`\n\tDisableDefaultPolicy bool `yaml:\"disableDefaultPolicy\"`\n\tAuth AuthConfig `yaml:\"auth\"`\n\tStuns []HostConfig `yaml:\"stuns\"`\n\tRelays RelaysConfig `yaml:\"relays\"`\n\tSignalURI string `yaml:\"signalUri\"`\n\tStore StoreConfig `yaml:\"store\"`\n\tReverseProxy ReverseProxyConfig `yaml:\"reverseProxy\"`\n}\n\n// AuthConfig contains authentication/identity provider settings\ntype AuthConfig struct {\n\tIssuer string `yaml:\"issuer\"`\n\tLocalAuthDisabled bool `yaml:\"localAuthDisabled\"`\n\tSignKeyRefreshEnabled bool `yaml:\"signKeyRefreshEnabled\"`\n\tStorage AuthStorageConfig `yaml:\"storage\"`\n\tDashboardRedirectURIs []string `yaml:\"dashboardRedirectURIs\"`\n\tCLIRedirectURIs []string `yaml:\"cliRedirectURIs\"`\n\tOwner *AuthOwnerConfig `yaml:\"owner,omitempty\"`\n}\n\n// AuthStorageConfig contains auth storage settings\ntype AuthStorageConfig struct {\n\tType string `yaml:\"type\"`\n\tFile string `yaml:\"file\"`\n}\n\n// AuthOwnerConfig contains initial admin user settings\ntype AuthOwnerConfig struct {\n\tEmail string `yaml:\"email\"`\n\tPassword string `yaml:\"password\"`\n}\n\n// HostConfig represents a STUN/TURN/Signal host\ntype HostConfig struct {\n\tURI string `yaml:\"uri\"`\n\tProto string `yaml:\"proto,omitempty\"` // udp, dtls, tcp, http, https - defaults based on URI scheme\n\tUsername string `yaml:\"username,omitempty\"`\n\tPassword string `yaml:\"password,omitempty\"`\n}\n\n// RelaysConfig contains external relay server settings for clients\ntype RelaysConfig struct {\n\tAddresses []string `yaml:\"addresses\"`\n\tCredentialsTTL string `yaml:\"credentialsTTL\"`\n\tSecret string `yaml:\"secret\"`\n}\n\n// StoreConfig contains database settings\ntype StoreConfig struct {\n\tEngine string `yaml:\"engine\"`\n\tEncryptionKey string `yaml:\"encryptionKey\"`\n\tDSN string `yaml:\"dsn\"` // Connection string for postgres or mysql engines\n\tFile string `yaml:\"file\"` // SQLite database file path (optional, defaults to dataDir)\n}\n\n// ReverseProxyConfig contains reverse proxy settings\ntype ReverseProxyConfig struct {\n\tTrustedHTTPProxies []string `yaml:\"trustedHTTPProxies\"`\n\tTrustedHTTPProxiesCount uint `yaml:\"trustedHTTPProxiesCount\"`\n\tTrustedPeers []string `yaml:\"trustedPeers\"`\n}\n\n// DefaultConfig returns a CombinedConfig with default values\nfunc DefaultConfig() *CombinedConfig {\n\treturn &CombinedConfig{\n\t\tServer: ServerConfig{\n\t\t\tListenAddress: \":443\",\n\t\t\tMetricsPort: 9090,\n\t\t\tHealthcheckAddress: \":9000\",\n\t\t\tLogLevel: \"info\",\n\t\t\tLogFile: \"console\",\n\t\t\tStunPorts: []int{3478},\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t\tRelay: RelayConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\tStun: StunConfig{\n\t\t\t\tEnabled: false,\n\t\t\t\tPorts: []int{3478},\n\t\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t\t},\n\t\t},\n\t\tSignal: SignalConfig{\n\t\t\t// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults\n\t\t},\n\t\tManagement: ManagementConfig{\n\t\t\tDataDir: \"/var/lib/netbird/\",\n\t\t\tAuth: AuthConfig{\n\t\t\t\tStorage: AuthStorageConfig{\n\t\t\t\t\tType: \"sqlite3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRelays: RelaysConfig{\n\t\t\t\tCredentialsTTL: \"12h\",\n\t\t\t},\n\t\t\tStore: StoreConfig{\n\t\t\t\tEngine: \"sqlite\",\n\t\t\t},\n\t\t},\n\t}\n}\n\n// hasRequiredSettings returns true if the configuration has the required server settings\nfunc (c *CombinedConfig) hasRequiredSettings() bool {\n\treturn c.Server.ExposedAddress != \"\"\n}\n\n// parseExposedAddress extracts protocol, host, and host:port from the exposed address\n// Input format: \"https://example.com:443\" or \"http://example.com:8080\" or \"example.com:443\"\n// Returns: protocol (\"https\" or \"http\"), hostname only, and host:port\nfunc parseExposedAddress(exposedAddress string) (protocol, hostname, hostPort string) {\n\t// Default to https if no protocol specified\n\tprotocol = \"https\"\n\thostPort = exposedAddress\n\n\t// Check for protocol prefix\n\tif strings.HasPrefix(exposedAddress, \"https://\") {\n\t\tprotocol = \"https\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"https://\")\n\t} else if strings.HasPrefix(exposedAddress, \"http://\") {\n\t\tprotocol = \"http\"\n\t\thostPort = strings.TrimPrefix(exposedAddress, \"http://\")\n\t}\n\n\t// Extract hostname (without port)\n\thostname = hostPort\n\tif host, _, err := net.SplitHostPort(hostPort); err == nil {\n\t\thostname = host\n\t}\n\n\treturn protocol, hostname, hostPort\n}\n\n// ApplySimplifiedDefaults populates internal relay/signal/management configs from server settings.\n// Management is always enabled. Signal, Relay, and STUN are enabled unless external\n// overrides are configured (server.signalUri, server.relays, server.stuns).\nfunc (c *CombinedConfig) ApplySimplifiedDefaults() {\n\tif !c.hasRequiredSettings() {\n\t\treturn\n\t}\n\n\t// Parse exposed address to extract protocol and hostname\n\texposedProto, exposedHost, exposedHostPort := parseExposedAddress(c.Server.ExposedAddress)\n\n\t// Check for external service overrides\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\thasExternalSignal := c.Server.SignalURI != \"\"\n\thasExternalStuns := len(c.Server.Stuns) > 0\n\n\t// Default stunPorts to [3478] if not specified and no external STUN\n\tif len(c.Server.StunPorts) == 0 && !hasExternalStuns {\n\t\tc.Server.StunPorts = []int{3478}\n\t}\n\n\tc.applyRelayDefaults(exposedProto, exposedHostPort, hasExternalRelay, hasExternalStuns)\n\tc.applySignalDefaults(hasExternalSignal)\n\tc.applyManagementDefaults(exposedHost)\n\n\t// Auto-configure client settings (stuns, relays, signalUri)\n\tc.autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort, hasExternalStuns, hasExternalRelay, hasExternalSignal)\n}\n\n// applyRelayDefaults configures the relay service if no external relay is configured.\nfunc (c *CombinedConfig) applyRelayDefaults(exposedProto, exposedHostPort string, hasExternalRelay, hasExternalStuns bool) {\n\tif hasExternalRelay {\n\t\treturn\n\t}\n\n\tc.Relay.Enabled = true\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\tc.Relay.ExposedAddress = fmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort)\n\tc.Relay.AuthSecret = c.Server.AuthSecret\n\tif c.Relay.LogLevel == \"\" {\n\t\tc.Relay.LogLevel = c.Server.LogLevel\n\t}\n\n\t// Enable local STUN only if no external STUN servers and stunPorts are configured\n\tif !hasExternalStuns && len(c.Server.StunPorts) > 0 {\n\t\tc.Relay.Stun.Enabled = true\n\t\tc.Relay.Stun.Ports = c.Server.StunPorts\n\t\tif c.Relay.Stun.LogLevel == \"\" {\n\t\t\tc.Relay.Stun.LogLevel = c.Server.LogLevel\n\t\t}\n\t}\n}\n\n// applySignalDefaults configures the signal service if no external signal is configured.\nfunc (c *CombinedConfig) applySignalDefaults(hasExternalSignal bool) {\n\tif hasExternalSignal {\n\t\treturn\n\t}\n\n\tc.Signal.Enabled = true\n\tif c.Signal.LogLevel == \"\" {\n\t\tc.Signal.LogLevel = c.Server.LogLevel\n\t}\n}\n\n// applyManagementDefaults configures the management service (always enabled).\nfunc (c *CombinedConfig) applyManagementDefaults(exposedHost string) {\n\tc.Management.Enabled = true\n\tif c.Management.LogLevel == \"\" {\n\t\tc.Management.LogLevel = c.Server.LogLevel\n\t}\n\tif c.Management.DataDir == \"\" || c.Management.DataDir == \"/var/lib/netbird/\" {\n\t\tc.Management.DataDir = c.Server.DataDir\n\t}\n\tc.Management.DnsDomain = exposedHost\n\tc.Management.DisableAnonymousMetrics = c.Server.DisableAnonymousMetrics\n\tc.Management.DisableGeoliteUpdate = c.Server.DisableGeoliteUpdate\n\t// Copy auth config from server if management auth issuer is not set\n\tif c.Management.Auth.Issuer == \"\" && c.Server.Auth.Issuer != \"\" {\n\t\tc.Management.Auth = c.Server.Auth\n\t}\n\n\t// Copy store config from server if not set\n\tif c.Management.Store.Engine == \"\" || c.Management.Store.Engine == \"sqlite\" {\n\t\tif c.Server.Store.Engine != \"\" {\n\t\t\tc.Management.Store = c.Server.Store\n\t\t}\n\t}\n\n\t// Copy reverse proxy config from server\n\tif len(c.Server.ReverseProxy.TrustedHTTPProxies) > 0 || c.Server.ReverseProxy.TrustedHTTPProxiesCount > 0 || len(c.Server.ReverseProxy.TrustedPeers) > 0 {\n\t\tc.Management.ReverseProxy = c.Server.ReverseProxy\n\t}\n}\n\n// autoConfigureClientSettings sets up STUN/relay/signal URIs for clients\n// External overrides from server config take precedence over auto-generated values\nfunc (c *CombinedConfig) autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort string, hasExternalStuns, hasExternalRelay, hasExternalSignal bool) {\n\t// Determine relay protocol from exposed protocol\n\trelayProto := \"rel\"\n\tif exposedProto == \"https\" {\n\t\trelayProto = \"rels\"\n\t}\n\n\t// Configure STUN servers for clients\n\tif hasExternalStuns {\n\t\t// Use external STUN servers from server config\n\t\tc.Management.Stuns = c.Server.Stuns\n\t} else if len(c.Server.StunPorts) > 0 && len(c.Management.Stuns) == 0 {\n\t\t// Auto-configure local STUN servers for all ports\n\t\tfor _, port := range c.Server.StunPorts {\n\t\t\tc.Management.Stuns = append(c.Management.Stuns, HostConfig{\n\t\t\t\tURI: fmt.Sprintf(\"stun:%s:%d\", exposedHost, port),\n\t\t\t})\n\t\t}\n\t}\n\n\t// Configure relay for clients\n\tif hasExternalRelay {\n\t\t// Use external relay config from server\n\t\tc.Management.Relays = c.Server.Relays\n\t} else if len(c.Management.Relays.Addresses) == 0 {\n\t\t// Auto-configure local relay\n\t\tc.Management.Relays.Addresses = []string{\n\t\t\tfmt.Sprintf(\"%s://%s\", relayProto, exposedHostPort),\n\t\t}\n\t}\n\tif c.Management.Relays.Secret == \"\" {\n\t\tc.Management.Relays.Secret = c.Server.AuthSecret\n\t}\n\tif c.Management.Relays.CredentialsTTL == \"\" {\n\t\tc.Management.Relays.CredentialsTTL = \"12h\"\n\t}\n\n\t// Configure signal for clients\n\tif hasExternalSignal {\n\t\t// Use external signal URI from server config\n\t\tc.Management.SignalURI = c.Server.SignalURI\n\t} else if c.Management.SignalURI == \"\" {\n\t\t// Auto-configure local signal\n\t\tc.Management.SignalURI = fmt.Sprintf(\"%s://%s\", exposedProto, exposedHostPort)\n\t}\n}\n\n// LoadConfig loads configuration from a YAML file\nfunc LoadConfig(configPath string) (*CombinedConfig, error) {\n\tcfg := DefaultConfig()\n\n\tif configPath == \"\" {\n\t\treturn cfg, nil\n\t}\n\n\tdata, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read config file: %w\", err)\n\t}\n\n\tif err := yaml.Unmarshal(data, cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse config file: %w\", err)\n\t}\n\n\t// Populate internal configs from server settings\n\tcfg.ApplySimplifiedDefaults()\n\n\treturn cfg, nil\n}\n\n// Validate validates the configuration\nfunc (c *CombinedConfig) Validate() error {\n\tif c.Server.ExposedAddress == \"\" {\n\t\treturn fmt.Errorf(\"server.exposedAddress is required\")\n\t}\n\tif c.Server.DataDir == \"\" {\n\t\treturn fmt.Errorf(\"server.dataDir is required\")\n\t}\n\n\t// Validate STUN ports\n\tseen := make(map[int]bool)\n\tfor _, port := range c.Server.StunPorts {\n\t\tif port <= 0 || port > 65535 {\n\t\t\treturn fmt.Errorf(\"invalid server.stunPorts value %d: must be between 1 and 65535\", port)\n\t\t}\n\t\tif seen[port] {\n\t\t\treturn fmt.Errorf(\"duplicate STUN port %d in server.stunPorts\", port)\n\t\t}\n\t\tseen[port] = true\n\t}\n\n\t// authSecret is required only if running local relay (no external relay configured)\n\thasExternalRelay := len(c.Server.Relays.Addresses) > 0\n\tif !hasExternalRelay && c.Server.AuthSecret == \"\" {\n\t\treturn fmt.Errorf(\"server.authSecret is required when running local relay\")\n\t}\n\n\treturn nil\n}\n\n// HasTLSCert returns true if TLS certificate files are configured\nfunc (c *CombinedConfig) HasTLSCert() bool {\n\treturn c.Server.TLS.CertFile != \"\" && c.Server.TLS.KeyFile != \"\"\n}\n\n// HasLetsEncrypt returns true if Let's Encrypt is configured\nfunc (c *CombinedConfig) HasLetsEncrypt() bool {\n\treturn c.Server.TLS.LetsEncrypt.Enabled &&\n\t\tc.Server.TLS.LetsEncrypt.DataDir != \"\" &&\n\t\tlen(c.Server.TLS.LetsEncrypt.Domains) > 0\n}\n\n// parseExplicitProtocol parses an explicit protocol string to nbconfig.Protocol\nfunc parseExplicitProtocol(proto string) (nbconfig.Protocol, bool) {\n\tswitch strings.ToLower(proto) {\n\tcase \"udp\":\n\t\treturn nbconfig.UDP, true\n\tcase \"dtls\":\n\t\treturn nbconfig.DTLS, true\n\tcase \"tcp\":\n\t\treturn nbconfig.TCP, true\n\tcase \"http\":\n\t\treturn nbconfig.HTTP, true\n\tcase \"https\":\n\t\treturn nbconfig.HTTPS, true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\n// parseStunProtocol determines protocol for STUN/TURN servers.\n// stun: → UDP, stuns: → DTLS, turn: → UDP, turns: → DTLS\n// Explicit proto overrides URI scheme. Defaults to UDP.\nfunc parseStunProtocol(uri, proto string) nbconfig.Protocol {\n\tif proto != \"\" {\n\t\tif p, ok := parseExplicitProtocol(proto); ok {\n\t\t\treturn p\n\t\t}\n\t}\n\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"stuns:\"):\n\t\treturn nbconfig.DTLS\n\tcase strings.HasPrefix(uri, \"turns:\"):\n\t\treturn nbconfig.DTLS\n\tdefault:\n\t\t// stun:, turn:, or no scheme - default to UDP\n\t\treturn nbconfig.UDP\n\t}\n}\n\n// parseSignalProtocol determines protocol for Signal servers.\n// https:// → HTTPS, http:// → HTTP. Defaults to HTTPS.\nfunc parseSignalProtocol(uri string) nbconfig.Protocol {\n\turi = strings.ToLower(uri)\n\tswitch {\n\tcase strings.HasPrefix(uri, \"http://\"):\n\t\treturn nbconfig.HTTP\n\tdefault:\n\t\t// https:// or no scheme - default to HTTPS\n\t\treturn nbconfig.HTTPS\n\t}\n}\n\n// stripSignalProtocol removes the protocol prefix from a signal URI.\n// Returns just the host:port (e.g., \"selfhosted2.demo.netbird.io:443\").\nfunc stripSignalProtocol(uri string) string {\n\turi = strings.TrimPrefix(uri, \"https://\")\n\turi = strings.TrimPrefix(uri, \"http://\")\n\treturn uri\n}\n\nfunc buildRelayConfig(relays RelaysConfig) (*nbconfig.Relay, error) {\n\tvar ttl time.Duration\n\tif relays.CredentialsTTL != \"\" {\n\t\tvar err error\n\t\tttl, err = time.ParseDuration(relays.CredentialsTTL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid relay credentials TTL %q: %w\", relays.CredentialsTTL, err)\n\t\t}\n\t}\n\treturn &nbconfig.Relay{\n\t\tAddresses: relays.Addresses,\n\t\tCredentialsTTL: util.Duration{Duration: ttl},\n\t\tSecret: relays.Secret,\n\t}, nil\n}\n\n// buildEmbeddedIdPConfig builds the embedded IdP configuration.\n// authStore overrides auth.storage when set.\nfunc (c *CombinedConfig) buildEmbeddedIdPConfig(mgmt ManagementConfig) (*idp.EmbeddedIdPConfig, error) {\n\tauthStorageType := mgmt.Auth.Storage.Type\n\tauthStorageDSN := c.Server.AuthStore.DSN\n\tif c.Server.AuthStore.Engine != \"\" {\n\t\tauthStorageType = c.Server.AuthStore.Engine\n\t}\n\tif authStorageType == \"\" {\n\t\tauthStorageType = \"sqlite3\"\n\t}\n\tauthStorageFile := \"\"\n\tif authStorageType == \"postgres\" {\n\t\tif authStorageDSN == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"authStore.dsn is required when authStore.engine is postgres\")\n\t\t}\n\t} else {\n\t\tauthStorageFile = path.Join(mgmt.DataDir, \"idp.db\")\n\t\tif c.Server.AuthStore.File != \"\" {\n\t\t\tauthStorageFile = c.Server.AuthStore.File\n\t\t}\n\t}\n\n\tcfg := &idp.EmbeddedIdPConfig{\n\t\tEnabled: true,\n\t\tIssuer: mgmt.Auth.Issuer,\n\t\tLocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,\n\t\tSignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,\n\t\tStorage: idp.EmbeddedStorageConfig{\n\t\t\tType: authStorageType,\n\t\t\tConfig: idp.EmbeddedStorageTypeConfig{\n\t\t\t\tFile: authStorageFile,\n\t\t\t\tDSN: authStorageDSN,\n\t\t\t},\n\t\t},\n\t\tDashboardRedirectURIs: mgmt.Auth.DashboardRedirectURIs,\n\t\tCLIRedirectURIs: mgmt.Auth.CLIRedirectURIs,\n\t}\n\n\tif mgmt.Auth.Owner != nil && mgmt.Auth.Owner.Email != \"\" {\n\t\tcfg.Owner = &idp.OwnerConfig{\n\t\t\tEmail: mgmt.Auth.Owner.Email,\n\t\t\tHash: mgmt.Auth.Owner.Password,\n\t\t}\n\t}\n\n\treturn cfg, nil\n}\n\n// ToManagementConfig converts CombinedConfig to management server config\nfunc (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {\n\tmgmt := c.Management\n\n\t// Build STUN hosts\n\tvar stuns []*nbconfig.Host\n\tfor _, s := range mgmt.Stuns {\n\t\tstuns = append(stuns, &nbconfig.Host{\n\t\t\tURI: s.URI,\n\t\t\tProto: parseStunProtocol(s.URI, s.Proto),\n\t\t\tUsername: s.Username,\n\t\t\tPassword: s.Password,\n\t\t})\n\t}\n\n\t// Build relay config\n\tvar relayConfig *nbconfig.Relay\n\tif len(mgmt.Relays.Addresses) > 0 || mgmt.Relays.Secret != \"\" {\n\t\trelay, err := buildRelayConfig(mgmt.Relays)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trelayConfig = relay\n\t}\n\n\t// Build signal config\n\tvar signalConfig *nbconfig.Host\n\tif mgmt.SignalURI != \"\" {\n\t\tsignalConfig = &nbconfig.Host{\n\t\t\tURI: stripSignalProtocol(mgmt.SignalURI),\n\t\t\tProto: parseSignalProtocol(mgmt.SignalURI),\n\t\t}\n\t}\n\n\t// Build store config\n\tstoreConfig := nbconfig.StoreConfig{\n\t\tEngine: types.Engine(mgmt.Store.Engine),\n\t}\n\n\t// Build reverse proxy config\n\treverseProxy := nbconfig.ReverseProxy{\n\t\tTrustedHTTPProxiesCount: mgmt.ReverseProxy.TrustedHTTPProxiesCount,\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedHTTPProxies {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedHTTPProxies = append(reverseProxy.TrustedHTTPProxies, prefix)\n\t\t}\n\t}\n\tfor _, p := range mgmt.ReverseProxy.TrustedPeers {\n\t\tif prefix, err := netip.ParsePrefix(p); err == nil {\n\t\t\treverseProxy.TrustedPeers = append(reverseProxy.TrustedPeers, prefix)\n\t\t}\n\t}\n\n\t// Build HTTP config (required, even if empty)\n\thttpConfig := &nbconfig.HttpServerConfig{}\n\n\t// Build embedded IDP config (always enabled in combined server)\n\tembeddedIdP, err := c.buildEmbeddedIdPConfig(mgmt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Set HTTP config fields for embedded IDP\n\thttpConfig.AuthIssuer = mgmt.Auth.Issuer\n\thttpConfig.AuthAudience = \"netbird-dashboard\"\n\thttpConfig.AuthClientID = httpConfig.AuthAudience\n\thttpConfig.CLIAuthAudience = \"netbird-cli\"\n\thttpConfig.AuthUserIDClaim = \"sub\"\n\thttpConfig.AuthKeysLocation = mgmt.Auth.Issuer + \"/keys\"\n\thttpConfig.OIDCConfigEndpoint = mgmt.Auth.Issuer + \"/.well-known/openid-configuration\"\n\thttpConfig.IdpSignKeyRefreshEnabled = mgmt.Auth.SignKeyRefreshEnabled\n\tcallbackURL := strings.TrimSuffix(httpConfig.AuthIssuer, \"/oauth2\")\n\thttpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull\n\n\treturn &nbconfig.Config{\n\t\tStuns: stuns,\n\t\tRelay: relayConfig,\n\t\tSignal: signalConfig,\n\t\tDatadir: mgmt.DataDir,\n\t\tDataStoreEncryptionKey: mgmt.Store.EncryptionKey,\n\t\tHttpConfig: httpConfig,\n\t\tStoreConfig: storeConfig,\n\t\tReverseProxy: reverseProxy,\n\t\tDisableDefaultPolicy: mgmt.DisableDefaultPolicy,\n\t\tEmbeddedIdP: embeddedIdP,\n\t}, nil\n}\n\n// ApplyEmbeddedIdPConfig applies embedded IdP configuration to the management config.\n// This mirrors the logic in management/cmd/management.go ApplyEmbeddedIdPConfig.\nfunc ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config, mgmtPort int, disableSingleAccMode bool) error {\n\tif cfg.EmbeddedIdP == nil || !cfg.EmbeddedIdP.Enabled {\n\t\treturn nil\n\t}\n\n\t// Embedded IdP requires single account mode\n\tif disableSingleAccMode {\n\t\treturn fmt.Errorf(\"embedded IdP requires single account mode; multiple account mode is not supported with embedded IdP\")\n\t}\n\n\t// Set LocalAddress for embedded IdP, used for internal JWT validation\n\tcfg.EmbeddedIdP.LocalAddress = fmt.Sprintf(\"localhost:%d\", mgmtPort)\n\n\t// Set storage defaults based on Datadir\n\tif cfg.EmbeddedIdP.Storage.Type == \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Type = \"sqlite3\"\n\t}\n\tif cfg.EmbeddedIdP.Storage.Config.File == \"\" && cfg.Datadir != \"\" {\n\t\tcfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, \"idp.db\")\n\t}\n\n\tissuer := cfg.EmbeddedIdP.Issuer\n\n\t// Ensure HttpConfig exists\n\tif cfg.HttpConfig == nil {\n\t\tcfg.HttpConfig = &nbconfig.HttpServerConfig{}\n\t}\n\n\t// Set HttpConfig values from EmbeddedIdP\n\tcfg.HttpConfig.AuthIssuer = issuer\n\tcfg.HttpConfig.AuthAudience = \"netbird-dashboard\"\n\tcfg.HttpConfig.CLIAuthAudience = \"netbird-cli\"\n\tcfg.HttpConfig.AuthUserIDClaim = \"sub\"\n\tcfg.HttpConfig.AuthKeysLocation = issuer + \"/keys\"\n\tcfg.HttpConfig.OIDCConfigEndpoint = issuer + \"/.well-known/openid-configuration\"\n\tcfg.HttpConfig.IdpSignKeyRefreshEnabled = true\n\n\treturn nil\n}\n\n// EnsureEncryptionKey generates an encryption key if not set.\n// Unlike management server, we don't write back to the config file.\nfunc EnsureEncryptionKey(ctx context.Context, cfg *nbconfig.Config) error {\n\tif cfg.DataStoreEncryptionKey != \"\" {\n\t\treturn nil\n\t}\n\n\tlog.WithContext(ctx).Infof(\"DataStoreEncryptionKey is not set, generating a new key\")\n\tkey, err := crypt.GenerateKey()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate datastore encryption key: %v\", err)\n\t}\n\tcfg.DataStoreEncryptionKey = key\n\tkeyPreview := key[:8] + \"...\"\n\tlog.WithContext(ctx).Warnf(\"DataStoreEncryptionKey generated (%s); add it to your config file under 'server.store.encryptionKey' to persist across restarts\", keyPreview)\n\n\treturn nil\n}\n\n// LogConfigInfo logs informational messages about the loaded configuration\nfunc LogConfigInfo(cfg *nbconfig.Config) {\n\tif cfg.EmbeddedIdP != nil && cfg.EmbeddedIdP.Enabled {\n\t\tlog.Infof(\"running with the embedded IdP: %v\", cfg.EmbeddedIdP.Issuer)\n\t}\n\tif cfg.Relay != nil {\n\t\tlog.Infof(\"Relay addresses: %v\", cfg.Relay.Addresses)\n\t}\n}\n","structuredPatch":[{"oldStart":572,"oldLines":6,"newStart":572,"newLines":9,"lines":[" authStorageFile = path.Join(mgmt.DataDir, \"idp.db\")"," if c.Server.AuthStore.File != \"\" {"," authStorageFile = c.Server.AuthStore.File","+ if !filepath.IsAbs(authStorageFile) {","+ authStorageFile = filepath.Join(mgmt.DataDir, authStorageFile)","+ }"," }"," }"," "]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"9d500222-898a-4d81-8081-a69583b5c5c0"} +{"parentUuid":"204c217e-8a67-4b4f-8196-04c1753fc24c","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01BbgLKnDYbpS6vCUeujQv1t","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_013HuMf28esjzjdDvir6enSW","name":"Bash","input":{"command":"go build ./combined/... 2>&1","description":"Verify combined builds"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":324,"cache_read_input_tokens":88244,"output_tokens":101,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":324,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYft7zMurMz7ndVoLZDvi","type":"assistant","uuid":"a7cccfad-72d8-4d38-9f3f-3480feb18c27","timestamp":"2026-03-03T09:07:12.842Z"} +{"parentUuid":"a7cccfad-72d8-4d38-9f3f-3480feb18c27","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":3,"totalLines":0,"totalBytes":0,"taskId":"bf4k23iix"},"toolUseID":"bash-progress-0","parentToolUseID":"toolu_013HuMf28esjzjdDvir6enSW","uuid":"bf91e4f3-7e99-461a-8988-05ea2bb0ab49","timestamp":"2026-03-03T09:07:15.920Z"} +{"parentUuid":"bf91e4f3-7e99-461a-8988-05ea2bb0ab49","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":4,"totalLines":0,"totalBytes":0,"taskId":"bf4k23iix"},"toolUseID":"bash-progress-1","parentToolUseID":"toolu_013HuMf28esjzjdDvir6enSW","uuid":"d8c7933a-9dd6-48a9-8de6-0bf7ef502a67","timestamp":"2026-03-03T09:07:16.919Z"} +{"parentUuid":"d8c7933a-9dd6-48a9-8de6-0bf7ef502a67","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":5,"totalLines":0,"totalBytes":0,"taskId":"bf4k23iix"},"toolUseID":"bash-progress-2","parentToolUseID":"toolu_013HuMf28esjzjdDvir6enSW","uuid":"40c0cdf9-00cd-416d-8124-14abf387ed11","timestamp":"2026-03-03T09:07:17.921Z"} +{"parentUuid":"40c0cdf9-00cd-416d-8124-14abf387ed11","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":6,"totalLines":0,"totalBytes":0,"taskId":"bf4k23iix"},"toolUseID":"bash-progress-3","parentToolUseID":"toolu_013HuMf28esjzjdDvir6enSW","uuid":"5cc8d509-4be3-43ab-99b7-dbb739c626a4","timestamp":"2026-03-03T09:07:18.922Z"} +{"parentUuid":"5cc8d509-4be3-43ab-99b7-dbb739c626a4","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":7,"totalLines":0,"totalBytes":0,"taskId":"bf4k23iix"},"toolUseID":"bash-progress-4","parentToolUseID":"toolu_013HuMf28esjzjdDvir6enSW","uuid":"6ecbaaf0-a557-4944-93ff-bf97957c345c","timestamp":"2026-03-03T09:07:19.922Z"} +{"parentUuid":"6ecbaaf0-a557-4944-93ff-bf97957c345c","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":8,"totalLines":0,"totalBytes":0,"taskId":"bf4k23iix"},"toolUseID":"bash-progress-5","parentToolUseID":"toolu_013HuMf28esjzjdDvir6enSW","uuid":"f0123ddb-6f5d-4c28-ad69-9bb57fcd326b","timestamp":"2026-03-03T09:07:20.925Z"} +{"parentUuid":"f0123ddb-6f5d-4c28-ad69-9bb57fcd326b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":9,"totalLines":0,"totalBytes":0,"taskId":"bf4k23iix"},"toolUseID":"bash-progress-6","parentToolUseID":"toolu_013HuMf28esjzjdDvir6enSW","uuid":"75b2e0c9-0019-4931-a64d-1ee83b2dc35f","timestamp":"2026-03-03T09:07:21.926Z"} +{"parentUuid":"75b2e0c9-0019-4931-a64d-1ee83b2dc35f","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":10,"totalLines":0,"totalBytes":0,"taskId":"bf4k23iix"},"toolUseID":"bash-progress-7","parentToolUseID":"toolu_013HuMf28esjzjdDvir6enSW","uuid":"8c30314d-37da-4d8a-a923-d49fda6617b2","timestamp":"2026-03-03T09:07:22.927Z"} +{"parentUuid":"8c30314d-37da-4d8a-a923-d49fda6617b2","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":11,"totalLines":0,"totalBytes":0,"taskId":"bf4k23iix"},"toolUseID":"bash-progress-8","parentToolUseID":"toolu_013HuMf28esjzjdDvir6enSW","uuid":"a1943e29-59db-435a-bc84-05ca66ac611b","timestamp":"2026-03-03T09:07:23.929Z"} +{"parentUuid":"a1943e29-59db-435a-bc84-05ca66ac611b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":12,"totalLines":0,"totalBytes":0,"taskId":"bf4k23iix"},"toolUseID":"bash-progress-9","parentToolUseID":"toolu_013HuMf28esjzjdDvir6enSW","uuid":"ab80e1f6-1e14-4c46-99ea-a03d65dd4532","timestamp":"2026-03-03T09:07:24.930Z"} +{"parentUuid":"ab80e1f6-1e14-4c46-99ea-a03d65dd4532","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":13,"totalLines":0,"totalBytes":0,"taskId":"bf4k23iix"},"toolUseID":"bash-progress-10","parentToolUseID":"toolu_013HuMf28esjzjdDvir6enSW","uuid":"18e213d0-d5f3-4129-8604-39745f02c422","timestamp":"2026-03-03T09:07:25.933Z"} +{"parentUuid":"a7cccfad-72d8-4d38-9f3f-3480feb18c27","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_013HuMf28esjzjdDvir6enSW","type":"tool_result","content":"","is_error":false}]},"uuid":"a9fc8db4-95a9-4add-8425-8a6ca543586d","timestamp":"2026-03-03T09:07:26.102Z","toolUseResult":{"stdout":"","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"a7cccfad-72d8-4d38-9f3f-3480feb18c27"} +{"parentUuid":"a9fc8db4-95a9-4add-8425-8a6ca543586d","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_018VrPrui39xmLg8QACrFgFd","type":"message","role":"assistant","content":[{"type":"text","text":"Fixed. Relative paths like `\"custom_idp.db\"` are now resolved against `mgmt.DataDir` (e.g. `/var/lib/netbird/custom_idp.db`), while absolute paths like `\"/data/idp.db\"` are used as-is. This matches how the main store and activity store handle relative paths via `filepath.IsAbs` checks in their respective `NewSqliteStore` and `initDatabase` functions."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":107,"cache_read_input_tokens":88568,"output_tokens":109,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":107,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYft9ca3HKMPtfcnBAies","type":"assistant","uuid":"8efa6ce1-50ae-483a-8833-46694c0bf1f6","timestamp":"2026-03-03T09:07:36.070Z"} +{"parentUuid":"8efa6ce1-50ae-483a-8833-46694c0bf1f6","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"Stop","hookName":"Stop","command":"entire hooks claude-code stop"},"parentToolUseID":"967e9105-2d7a-4e03-87a3-ba4143582232","toolUseID":"967e9105-2d7a-4e03-87a3-ba4143582232","timestamp":"2026-03-03T09:07:36.188Z","uuid":"f70b503c-06ef-4651-be5f-732e46bec7ef"} +{"parentUuid":"f70b503c-06ef-4651-be5f-732e46bec7ef","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"system","subtype":"stop_hook_summary","hookCount":1,"hookInfos":[{"command":"entire hooks claude-code stop","durationMs":2826}],"hookErrors":[],"preventedContinuation":false,"stopReason":"","hasOutput":true,"level":"suggestion","timestamp":"2026-03-03T09:07:39.018Z","uuid":"9573d920-ff87-46ce-853d-52ee75562949","toolUseID":"967e9105-2d7a-4e03-87a3-ba4143582232"} +{"parentUuid":"9573d920-ff87-46ce-853d-52ee75562949","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"system","subtype":"turn_duration","durationMs":81158,"timestamp":"2026-03-03T09:07:39.020Z","uuid":"9e0e87dd-82d5-4df3-9b7a-dee988ff05a4","isMeta":false} +{"type":"file-history-snapshot","messageId":"ff61ba0a-b95e-4076-bf49-5f9944fb152b","snapshot":{"messageId":"ff61ba0a-b95e-4076-bf49-5f9944fb152b","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v3","version":3,"backupTime":"2026-03-03T09:45:34.548Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v2","version":2,"backupTime":"2026-03-02T19:20:06.400Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v2","version":2,"backupTime":"2026-03-02T19:45:28.129Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/config.yaml.example":{"backupFileName":"ab8acefcb43dda84@v2","version":2,"backupTime":"2026-03-02T19:45:28.132Z"}},"timestamp":"2026-03-03T09:45:34.545Z"},"isSnapshotUpdate":false} +{"parentUuid":"9e0e87dd-82d5-4df3-9b7a-dee988ff05a4","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":" Relative paths like \"custom_idp.db\" are now resolved against mgmt.DataDir"},"uuid":"ff61ba0a-b95e-4076-bf49-5f9944fb152b","timestamp":"2026-03-03T09:45:32.839Z","permissionMode":"default"} +{"parentUuid":"ff61ba0a-b95e-4076-bf49-5f9944fb152b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01MtFwLGGWJxQfchS1oznjZC","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user seems to be pointing out something about relative path resolution. Let me think about what they might be asking...\n\nThey quoted my statement: \"Relative paths like 'custom_idp.db' are now resolved against mgmt.DataDir\"\n\nThey might be pointing out that the same should be true for the other two stores (main store and activity store) when the env vars are set in `root.go` and `token.go`. Let me check the current code there - are we resolving relative paths before setting the env vars?\n\nLooking at `root.go`:\n```go\nif file := config.Server.Store.File; file != \"\" {\n\n\n```\n\nAnd `token.go`:\n```go\nif file := cfg.Server.Store.File; file != \"\" {\n```\n\nI see the issue now - in both places, we're setting the environment variables with the file paths directly without resolving relative paths first. The downstream stores like `NewSqliteStore` would then receive these unresolved paths, which means relative paths wouldn't be handled consistently across all three stores the way they are for the custom IDP store. for the store and activity store env var settings in root.go and token.go as well. But actually, the downstream stores already handle relative paths correctly by checking `filepath.IsAbs` and joining with `dataDir`, and since they all use the same directory (`mgmt.DataDir`), the behavior is already consistent across all three stores.\n\nThe user might just be asking me to confirm this, or maybe they're wondering if root.go and token.go should also be doing explicit path resolution. Let me think through the flow: in root.go we set the env var with the raw file path, then `NewSqliteStore` resolves any relative paths against the `dataDir` parameter it receives. This works fine since the relative path resolution happens downstream, and there's no scenario where the env var gets consumed by something that doesn't handle relative paths correctly.","signature":"Ep4hCkYICxgCKkAFhRQPvg/lw2ls/Cj8wGJr7DkB8hxUecbKF9yNCYqWqE16BZ+JyyZHbBwF/X/PMUycjmS4H7sgBH0PUru+0pTQEgwmauwhv4L9c47ZWGAaDE7swaPH9Z8AQDkwMSIwTXgS0Tszhm5umeW6QcHaDzJ2RWJbR6MdO+1EZ52+Au30QoSrhQiBgeqcr6PEGtp2KoUgYwlrihfVfhB+agCornzfxPntSXdpoL5LKtFykQWbhUboFF/fL/FePDgCOLRpVPeRfvA5r4wZrtN3fx2CYslof+lACGwPVbr7DVRGRGmzQPjAY1G7uGQye0yIWPIeIMfU4JNM2G0EwjXy+A43kIhkJst/ICoEpiNG8wmbEWtHCPkKd4OwCT/rqOMWSOBjn7k1ZGuNlNJqBH9IMOIHRXFzVh3twOsmjjsemP1hoRcpgACJJ+aw8mbSvLGwNpP2RRfhGP/Qocjn1hCQkJv1K6vaoSbJvOoY0kYrZaPFYgRMQ4ViCDq+UCCuLrNXMTHoxNRjsEf4V0edM9fBLRgyhjtUXVIaEo9rV2MFjQgVKCNU89KQD66TL4hFX9as821FxeLAB5phbEQaIfPQXv2wNzxwlfC5fHMnBTB3M3bLlZN30LzNM+aNo+195sM62WwznCRhSjauRphsk9sgpJVUAyAPe+f8qJSgVx5KkyV7ylZ5OnmKfh9jt/jywU5sAd6HAcyspVxZboczC0UzA217SjDTVb/LTjt8qyfyoHBiRTSwULYb3Vlsfrwmf9+3CV1Z+7/VUjb6niR8xm9M+GZ9NV5KnOQYY+om92BEozJEp8osNL/AmwgVpTw/JNrFp7nowMVTGoOWX8A4PfFDzfktGbSIU/67UqUlACArhe+cGFHkC8ZimqpjSKStquC26RTvYjCWNAlm7AAhlFfPIeI3os/Nc4RV0J50HanqARlchWl+nSUW5XQagGLBCz4o3yUYNL1/1fbeO5HgQJAbkmobVRavDyYiblVnZhV64GG7k4dw0NeEkvlDSXN1+726orbSdbuWtNtOQuSB5KD6je9W7Ha8M/HkJuaGLWilTbHB6CQy9XonlPsuM82kywr7ZHl/oymEJxJH5506KOLpNar8b/+fw/TRpTh8adCYZ5HCLuuUNKYi3PCVlTCCk5m8JQwLtMFr4Q2s50LHX3SpJAC89KNkgeG6vW+ZQ90xqM3BdCtHcdsX2uH3N/T14N47UUgHDUGugvEmduSCw3ScO+QHlRiQJXX9Ed2OUtwLV5qmhdukc7pQ9N+/MHPjbc2rnYXA0IYg7raE7XlBmZQ6GHlo17AKUvf+s5b6QqKDCy8ulxOhnk76jK0Ijlr7zm5YfvzhSxAHiL7x7igl3hv+i2qtL4WR/C96QFv7X0O0uyJPAdOacmgZi2ZmMArrjiIOH79NcCSPLLJZ0/ZgzHXeh0XurEQEQUKMpoKraDx7U09mvqSxQH4YZwzZvknwCcxlxxuXeIw3hM6JQCMg3q8sHn403qzc3No8IdWDqBRQobwEXQdKZcG1saYljzBJbglrrJ43C9iG+sxfTKfwmC1yJlsJDZUmeH2ajfmvWzNARVhwPcpuCUEJuMxKDtkEhQlqfIMZIFfP5eZt/g5zdt7dBhLZORxVu12cFORVEx9wrpSK6GoRmQ/sFpXEEs3eKuOu8PfAFvjm8Ig6peUMyWjFP0+cXZlyICy75o+6y1xqJsCcun4uhPNX3Wlfb+XOhd6+H//lAYuQ7feVQHv+6g0bFGAFS8JZ0Ep+sELU85b5J9aB8IsKLyX4qHFu6g97J97vDRy2mEuRsQTCWryepBrXJmadvLkNshOSBbgXcS24xqfBeY28KmTOiQUx8iw+P65megTaogr7VdO73F82un1pPCyTjce1Lli4HFCG5M2eutq4v+AXpNYZz4n4hJIcepAYy3gcfeKXnQgqCL2PDCetycbv4I3MZLm3WZboaLbwrPlgX3n4ojdoCxTM1QGT/0dmpnEgkWb+kV0hM7IvN/d8ac5ERUzrUNgg9TtujQE34g+yKtJOgZcEKhNdroxA7zzG6ifVu2gP0+ZwXm+sxFd4KTUSHYpBnKduhFPafs6bKGlnkRfSjL2N7A65L4wWk68iMBKz4NzcIuFnRnKeE2AJcpJDHZWMQKn/lX974KexLhoKSN0Ue9a4GCk6zgwvcyJAreMPmmiRQ/IPzlOmdSSOgSbTR7+D9k7WSWA+CCbC4Rh/hV9BAhwIE8/k8p/ErzCt3kWh0fuiuuBdiY2/jRc/gRU2T0ZKE7/5zRVxGJkj2mqgC63JOxutBfmKt2xzey8VoIF+TjZRoPvuozr/edOp+Vusrq3EiE9f8Jj8ksM5+C8Vma3FoNcq3pOWtGvvTmaBntfgsK2g6+40dcNDhvO+iZwmiesu6crTryRXx/2Y1LEUj4wW4l/o9V91jpLLW5gBDIELicJUi4VjKh0WnULjzEEio7T5HnuLN55Fw01SAMBdEsAnCdPXpdwg/KDmTQwXpLzYjxi7A7x1CGAiC6b+q1rRFAyh0vG+LxFt9DKGBoM1BfHWMkd3yR6vjO3oDK13QA75u2BK3uEQH7sLJsKkAbnhTaoiIKUWZqv7/OGOM4JCK7Ss/77zqbaUw//UliYaQBfRBwI3B2An9Y2+tNEiBqxKdQgZ4RsB92oEg3li1BMIoyuoeBdOorU5UfZfHCSXbRlTKyqpaLQ5X++6dCL0C4KcwvhU5s1wh+fegIto8zrNJDlBfjc8poRtRAhk6jkSoz09sQcfhnspbx9FJ+/F0L01hEWQVkfhRD4igmK57v/3+i0yprl609KIg026lCIwz17LI8juOiRL4HrDe/AIPq9LuvHr1IJbnA5/7/6hS8zYF96ekM/aJ3lDHZ5cc0yudn7HdgFys1K1b5C+V509ofg2wQjp1wF3F4b7wgoTdW2TX0jl7yO5ojajxvzhqByXToZg5BWXM4mzrQGPJC/N1Xdp78Cd1HMTQsDvBS8CztBGFBZI6fgV9KuZWE/TChr24QIX+CyD7unbTh0q/RjFQ+CfeHVV/aHh+C8gIOn88nZ9HgE262Dz6lhnkRQ882nLodG16g4JtDiGTf7IdWxSMyAKIW8yiMOaTM/ZqxzJX4C8C4PSZ1jD8hO9XhMWiG6pjHrQ9K9d5sbcq8Caujppdgs7MFnEItNa6F83sQfowsJDVyRsBMjISkAKvjcduipod47MjnstDZjRuiyEeNoLG1+IB3iqlsOV+fZnyGggb+S5C+0yIzVc7SvHxncq1SoASVn7Q7aBjWktVnGjEvVFETB8LqzpedbGOSQIII0n0y/zMXuEMV+0MbVNLJjBZ8RyMDMngt4ZBftlAlpLf+4Ogpbqau2IMY9vvA709Av+rtYKT5X/kPidpyJ9VtJoc10amft24IOP6kYwykFuQRuQFyC3Tyova2HqD7T0gdz1eSJUOGOLOaf0vo+sVHaN7I+lxg5WQL2cLcn0PhriaKYjWalBM3qj7gpXegLEDZSpW8ftyMt8lM364DBnhyn/GbhcxsZgkQVyWwJEf1eC5IeAMidJTNcPwNveQUgbLmVxSUFK31jeD5n8uhx9sIAsba5GCELjsoSgQH6L8g22S/foR4k6IxmVFUiC20d4uyQ6NOJCck+1f+8rLZdQWaAMvV0J1rc2sGI0/4FS+gFU5n6ckM4i/PsuUqzZt3JU1bu1aWEBHLLsPY2mIK0gpYng23xJuCvBgCIqxiKfvO0E4btTt25k2lF9l6smw9jBJE1cgH1YMMeV++F2VWvzmFjeeKRj7OLDuOBXkd4Dz4RTNZJ6etYw03pO4lIx7SMwDEZyTPjh7tR/4RoFavRLZS848GiQNyk1EV+/x/3TGHhUkfRydyUiI0Zbxn7HqtydlhQ2ilU8ELtDb92cFp7KT3FdpUlW9D6wB3rksK0J6/U7tLcAWHVfRTNSyYmvaM59dpMfDyOALHpzWvgglP1fRGxBjQxAkuq+6hj/2oXu24aEfzoZI3Pf60X6tNbZUGwXuTJXvmTvJeysnVEBu4nvznnczMMmFHWhb3M1Z6UbHgznglLaCN6j2mRNVQH6QxDuTwn9l6Y9gNkzt30iFvccIGHwmnoDvspXLw+SA+QilkvW+jvBOFj4uae0sJDkfFJ88i2HUEwJNOM+KvzWMVaHdOGybd59/BJD5TJrwAFqm0zHV6wKKVvOX7QdgnyKhTHI9urr61OUmcmkUOAgvpWLhqpPOwQHj3F7jrF6rBZ8sfsP4vidap/wA+rfvgH+eTviUt8jFB3V9qOZdMIkm+WHfHWnVhtYporD7htzo2I7kaV33XhIsDIFqCsjSm9wIhrvXL8/VAyz+BO+jBkSWvBLMh0VlqZuGfKHQFQx05wZWENCHqVUavioIBNWVJgXPxP80KaIiAW45QIoQUFtj8UYUJdWVCmDkAGNw1AZ/b6/E9zr9lGXpJujrbZyIufX8tlGtzv7BR6z8EOls2ZflNRH/EdIZD/CqweIlgIaMmdB8mqAgHyoCV8ZH43rve4qby91l0NoUTDopRkmsISE9L+JEDoQM75Kd5GejGDPgwpnRj0ZXi9F1KQr7kbWlSAJzNqceTvyGHBFeFHxm81gKVHMzDXwyj7IYGHnG+hqK6FXgQtp2tWrgwgS3EI4XuF3EnkK2GvbMZlada4q4ldxIV2V4YEu6aMjRTvhr/pXi+NB1MvTI7drP/8TtwlXFPJq70Oubi0o27Y/W4CrwUHijIkWX72Qrrf4kgEKrNEkVxYYOYDjO72/0XpLWKIBO17vCTdWd/znU2JZUUYg+ysM0nmHXgah+xqOja10JQrbhtlZ7il3ECPBPEmnD/Au4iA2jM5n5ltnRQXTTX6gxKHfRJMdRXIGKoIA4zuoOXQq5GanmQk9vX4E1rKQHvu+NOGFamVqk0FUF4BXEIoy4ZOR2LgdbyoNwGcoViPSK5w+h1/urI7wdtIsUtAs8HGE+XTtjKydG+3wNqdmTiBZL4Zhzb8S2Sem1QmW0tZy+dKjDCPxH3+rYdTI3arZ6uGIQAiBOZoc2BVjZgF1igli1LQxfo7UCheXrFoRkwD/OdRxoJsa20832BvXPzR6l/DrXqfFrXmaAtTdgd335CgVDRGf7PvnRvCkU7AFcbtRmk8O0pkQoLU712kEnRgzxwdGNsCBbne+PpHt3DTHlfIJBeV1qeN8F8eim5iPxc52NTNR8gPrypZdAxfihbFrF46GplmXBqAJygP761OuqW25FSkS4F3X1PnQjylEO2NBp4qYVBKsX6PgGgK/hD0O/3ulY0mVggg2/qOzarImqkdCL/1UmggA6QM+k2Ypu8RBoADPWWapr0VM/bzYibYPndpfZzwMpqRoOdCGSc61NEtH9JdGn7q10Rp/aYy2cbeTTA6tevd1W3sD5v+jpT78u478yJRkKXkzUpa/xvjXfjFuUOJWQzKO48XkDOrvs4+rbREQVayJWgFi9hqAFm55ZNbnJiGr8Jg817HLDlOwPoU6TRZpEf71GOYbExLaiFPP8Ks0HScyUlpIBdm4QI1QbwfurVkt6dOSTtKthqpvqJi75CnlrxAbvjLf+j+x0Rm7jIidd1PIQbj014/M35adC3kwJjBUtOj26NPl4xjivq6Zdm0wA8PFPeuArklGiFKV0Oll1s6+NkcsIa2RWLZpdmWNMMMO/KP8GAE="}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":69136,"cache_read_input_tokens":19729,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":69136},"output_tokens":11,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYfw3wKLbGU9E3J1mMikr","type":"assistant","uuid":"9278d871-0382-42bb-b514-5d9b7599614e","timestamp":"2026-03-03T09:45:59.903Z"} +{"parentUuid":"9278d871-0382-42bb-b514-5d9b7599614e","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01MtFwLGGWJxQfchS1oznjZC","type":"message","role":"assistant","content":[{"type":"text","text":"Yes — and for the main store and activity store, relative path resolution happens the same way but downstream in the store code itself (`NewSqliteStore` and `initDatabase` both check `filepath.IsAbs` and join with `dataDir` if relative). The auth store was the only one that needed the fix in the combined config because its file path is set directly rather than going through an env var."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":69136,"cache_read_input_tokens":19729,"output_tokens":1160,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":69136,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYfw3wKLbGU9E3J1mMikr","type":"assistant","uuid":"3577dbdc-b391-4872-ae86-2600c2275dd2","timestamp":"2026-03-03T09:46:01.000Z"} +{"parentUuid":"3577dbdc-b391-4872-ae86-2600c2275dd2","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"Stop","hookName":"Stop","command":"entire hooks claude-code stop"},"parentToolUseID":"373d0a08-f790-4f52-a866-db6510ec42fa","toolUseID":"373d0a08-f790-4f52-a866-db6510ec42fa","timestamp":"2026-03-03T09:46:01.088Z","uuid":"d1bfb56c-708e-4613-8379-73a46dfcb5bc"} +{"parentUuid":"d1bfb56c-708e-4613-8379-73a46dfcb5bc","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"feature/add-sql-stores-file-paths","slug":"jaunty-mapping-trinket","type":"system","subtype":"stop_hook_summary","hookCount":1,"hookInfos":[{"command":"entire hooks claude-code stop","durationMs":782}],"hookErrors":[],"preventedContinuation":false,"stopReason":"","hasOutput":true,"level":"suggestion","timestamp":"2026-03-03T09:46:01.871Z","uuid":"87dcae71-ce52-47ba-828a-f16993b8fa08","toolUseID":"373d0a08-f790-4f52-a866-db6510ec42fa"} +{"type":"file-history-snapshot","messageId":"65dd8abd-350a-47be-a3e8-f0e903d86080","snapshot":{"messageId":"65dd8abd-350a-47be-a3e8-f0e903d86080","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v3","version":3,"backupTime":"2026-03-03T09:45:34.548Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v3","version":3,"backupTime":"2026-03-04T09:35:30.335Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v2","version":2,"backupTime":"2026-03-02T19:45:28.129Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/config.yaml.example":{"backupFileName":"ab8acefcb43dda84@v2","version":2,"backupTime":"2026-03-02T19:45:28.132Z"}},"timestamp":"2026-03-04T09:35:30.333Z"},"isSnapshotUpdate":false} +{"parentUuid":"87dcae71-ce52-47ba-828a-f16993b8fa08","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":"decode this dex user id: REDACTED"},"uuid":"65dd8abd-350a-47be-a3e8-f0e903d86080","timestamp":"2026-03-04T09:35:29.273Z","permissionMode":"default"} +{"parentUuid":"65dd8abd-350a-47be-a3e8-f0e903d86080","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_017nyzudEfsF1y6WzLekeCQY","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to decode a base64-encoded Dex user ID. Let me decode it.","signature":"EvMBCkYICxgCKkB0VOTtwoUCiElCcl8UE1CSjFS0m4OtsvZiP+8jvgdrbcb0qY+vVESDEjKBJaxClj+LOZCeKyzFLb4v5S+ncqNuEgwpPe0jcEYONG5aGqAaDAT4lQLxTXJpRt6DDyIw172JHjJc1sQHw1rtPPClkG4RY/WvJhK2ZphDnJEqqjNTjakK1FsAN8IJ2KbZOpCsKlvmnwEMgj/gT2OpKRPaSQshg7w4Sz8YfifaWQjbJIz2Mn3kqteGyPh7racQMYBT4CND+DjrpNAmrJ1Vt4x5imLdrusPu3f56FP1EgWEkT+f9K4HRWE3yMoXJrCdGAE="}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":79986,"cache_read_input_tokens":10426,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":79986},"output_tokens":11,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhp6KGh77b4KUWEzE4KX","type":"assistant","uuid":"54c67e1f-5bb7-4f96-b0e1-f8ddfcccf3d5","timestamp":"2026-03-04T09:35:34.930Z"} +{"parentUuid":"54c67e1f-5bb7-4f96-b0e1-f8ddfcccf3d5","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_017nyzudEfsF1y6WzLekeCQY","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_017GwezrdrJhi2aSAxduJrPL","name":"Bash","input":{"command":"echo \"REDACTED\" | base64 -d | cat -v","description":"Base64 decode the Dex user ID"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":79986,"cache_read_input_tokens":10426,"output_tokens":250,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":79986,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhp6KGh77b4KUWEzE4KX","type":"assistant","uuid":"571aea08-1ffb-45e4-b5d8-eaf515063530","timestamp":"2026-03-04T09:35:36.097Z"} +{"parentUuid":"571aea08-1ffb-45e4-b5d8-eaf515063530","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_017GwezrdrJhi2aSAxduJrPL","type":"tool_result","content":"@381d44eb2438e17b375c3c82151a6fcbbcdb0b937ad6b5893f3d2deba1e99c96^R^^authentik-d5v0e05ra0hc2alaoa","is_error":false}]},"uuid":"d935dbfa-29bf-4529-9f6d-e49ce0857983","timestamp":"2026-03-04T09:35:36.343Z","toolUseResult":{"stdout":"@381d44eb2438e17b375c3c82151a6fcbbcdb0b937ad6b5893f3d2deba1e99c96^R^^authentik-d5v0e05ra0hc2alaoa","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"571aea08-1ffb-45e4-b5d8-eaf515063530"} +{"parentUuid":"d935dbfa-29bf-4529-9f6d-e49ce0857983","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_0194ZnS659cGpqexkzMSxnfp","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_015eo6v89p1Cxivx6PMLDfed","name":"Bash","input":{"command":"echo \"REDACTED\" | base64 -d | xxd | head -20","description":"Hex dump to see protobuf structure"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":324,"cache_read_input_tokens":90412,"output_tokens":214,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":324,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhp6mGd5zRLeCTwY7jbC","type":"assistant","uuid":"881285a8-57d0-467c-b23b-4a735878e9d0","timestamp":"2026-03-04T09:35:42.410Z"} +{"parentUuid":"881285a8-57d0-467c-b23b-4a735878e9d0","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_015eo6v89p1Cxivx6PMLDfed","type":"tool_result","content":"00000000: 0a40 3338 3164 3434 6562 3234 3338 6531 .@381d44eb2438e1\n00000010: 3762 3337 3563 3363 3832 3135 3161 3666 7b375c3c82151a6f\n00000020: 6362 6263 6462 3062 3933 3761 6436 6235 cbbcdb0b937ad6b5\n00000030: 3839 3366 3364 3264 6562 6131 6539 3963 893f3d2deba1e99c\n00000040: 3936 121e 6175 7468 656e 7469 6b2d 6435 96..authentik-d5\n00000050: 7630 6530 3572 6130 6863 3261 6c61 6f61 v0e05ra0hc2alaoa","is_error":false}]},"uuid":"a75fac39-5e39-41f7-b6e7-6b03197e9d69","timestamp":"2026-03-04T09:37:21.238Z","toolUseResult":{"stdout":"00000000: 0a40 3338 3164 3434 6562 3234 3338 6531 .@381d44eb2438e1\n00000010: 3762 3337 3563 3363 3832 3135 3161 3666 7b375c3c82151a6f\n00000020: 6362 6263 6462 3062 3933 3761 6436 6235 cbbcdb0b937ad6b5\n00000030: 3839 3366 3364 3264 6562 6131 6539 3963 893f3d2deba1e99c\n00000040: 3936 121e 6175 7468 656e 7469 6b2d 6435 96..authentik-d5\n00000050: 7630 6530 3572 6130 6863 3261 6c61 6f61 v0e05ra0hc2alaoa","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"881285a8-57d0-467c-b23b-4a735878e9d0"} +{"parentUuid":"a75fac39-5e39-41f7-b6e7-6b03197e9d69","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01BbZirFtddszbUVBCgAVwk2","type":"message","role":"assistant","content":[{"type":"text","text":"This is a protobuf-encoded Dex user ID with two fields:\n\n- **Field 1** (user ID): `381d44eb2438e17b375c3c82151a6fcbbcdb0b937ad6b5893f3d2deba1e99c96`\n- **Field 2** (connector ID): `authentik-d5v0e05ra0hc2alaoa2g`\n\nThe `0a40` = field 1, length 64 bytes (the hex string). The `121e` = field 2, length 30 bytes (the connector name). This is the standard Dex `UserID` protobuf format combining the upstream user identifier with the connector that authenticated them."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":472,"cache_read_input_tokens":90736,"output_tokens":174,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":472,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhpEVgUr1Em56xy9sfH2","type":"assistant","uuid":"0c5c9e56-5beb-4954-a358-a63e293f4ec1","timestamp":"2026-03-04T09:37:27.619Z"} +{"parentUuid":"0c5c9e56-5beb-4954-a358-a63e293f4ec1","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"Stop","hookName":"Stop","command":"entire hooks claude-code stop"},"parentToolUseID":"5f2a094d-af55-454e-aa84-33b64f3a353a","toolUseID":"5f2a094d-af55-454e-aa84-33b64f3a353a","timestamp":"2026-03-04T09:37:27.714Z","uuid":"ce7e04c5-07d7-4e54-9162-cae3850b8f8c"} +{"parentUuid":"ce7e04c5-07d7-4e54-9162-cae3850b8f8c","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"system","subtype":"stop_hook_summary","hookCount":1,"hookInfos":[{"command":"entire hooks claude-code stop","durationMs":755}],"hookErrors":[],"preventedContinuation":false,"stopReason":"","hasOutput":true,"level":"suggestion","timestamp":"2026-03-04T09:37:28.470Z","uuid":"cc969ab5-5a0a-44e6-bcdf-d5ddfb76b991","toolUseID":"5f2a094d-af55-454e-aa84-33b64f3a353a"} +{"type":"file-history-snapshot","messageId":"b329a6d4-34f7-4b81-b06c-9cc221398e7a","snapshot":{"messageId":"b329a6d4-34f7-4b81-b06c-9cc221398e7a","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v3","version":3,"backupTime":"2026-03-03T09:45:34.548Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v3","version":3,"backupTime":"2026-03-04T09:35:30.335Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v2","version":2,"backupTime":"2026-03-02T19:45:28.129Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/config.yaml.example":{"backupFileName":"ab8acefcb43dda84@v2","version":2,"backupTime":"2026-03-02T19:45:28.132Z"}},"timestamp":"2026-03-04T09:37:58.382Z"},"isSnapshotUpdate":false} +{"parentUuid":"cc969ab5-5a0a-44e6-bcdf-d5ddfb76b991","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":"this one REDACTED and this one e1badda4-2a65-458c-aca0-b32c8e2b8a77"},"uuid":"b329a6d4-34f7-4b81-b06c-9cc221398e7a","timestamp":"2026-03-04T09:37:57.472Z","permissionMode":"default"} +{"parentUuid":"b329a6d4-34f7-4b81-b06c-9cc221398e7a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"type":"text","text":"[Request interrupted by user]"}]},"uuid":"78fa5d6f-99a1-4462-8e48-882c1ffad3ee","timestamp":"2026-03-04T09:37:58.581Z"} +{"type":"file-history-snapshot","messageId":"48451e99-10b4-416d-b5a9-bee9ad6fe6f3","snapshot":{"messageId":"48451e99-10b4-416d-b5a9-bee9ad6fe6f3","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v3","version":3,"backupTime":"2026-03-03T09:45:34.548Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v3","version":3,"backupTime":"2026-03-04T09:35:30.335Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v2","version":2,"backupTime":"2026-03-02T19:45:28.129Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/config.yaml.example":{"backupFileName":"ab8acefcb43dda84@v2","version":2,"backupTime":"2026-03-02T19:45:28.132Z"}},"timestamp":"2026-03-04T09:38:09.475Z"},"isSnapshotUpdate":false} +{"parentUuid":"78fa5d6f-99a1-4462-8e48-882c1ffad3ee","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":"this one REDACTED"},"uuid":"48451e99-10b4-416d-b5a9-bee9ad6fe6f3","timestamp":"2026-03-04T09:38:08.589Z","permissionMode":"default"} +{"parentUuid":"48451e99-10b4-416d-b5a9-bee9ad6fe6f3","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_0152cZEQwgUnky9VJ9Fia72H","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01X2RyNNAZgMoWkGfhwSEQdM","name":"Bash","input":{"command":"echo \"REDACTED\" | base64 -d | cat -v","description":"Base64 decode Dex user ID"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":63,"cache_read_input_tokens":91470,"output_tokens":145,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":63,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhpJ3orCFKHunp2izx3Y","type":"assistant","uuid":"f7f7cb10-79b3-4b7f-948d-1782eaa046f7","timestamp":"2026-03-04T09:38:13.501Z"} +{"parentUuid":"f7f7cb10-79b3-4b7f-948d-1782eaa046f7","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01X2RyNNAZgMoWkGfhwSEQdM","type":"tool_result","content":"$3371a2b9-6322-44ed-8de0-68e1460abed2^R^Elocal","is_error":false}]},"uuid":"deb001ea-34ba-4c34-a086-664c633ec875","timestamp":"2026-03-04T09:38:13.638Z","toolUseResult":{"stdout":"$3371a2b9-6322-44ed-8de0-68e1460abed2^R^Elocal","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"f7f7cb10-79b3-4b7f-948d-1782eaa046f7"} +{"parentUuid":"deb001ea-34ba-4c34-a086-664c633ec875","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_017u3yBhp4Gi3qpav7VBHtcg","type":"message","role":"assistant","content":[{"type":"text","text":"- **Field 1** (user ID): `3371a2b9-6322-44ed-8de0-68e1460abed2`\n- **Field 2** (connector ID): `local`\n\nThis is a local (embedded IdP) user with a UUID identifier."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":191,"cache_read_input_tokens":91533,"output_tokens":69,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":191,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhpJMfeAr5to7sn7HbnL","type":"assistant","uuid":"06426988-71dd-4da8-885b-1b63861e8ef1","timestamp":"2026-03-04T09:38:17.502Z"} +{"parentUuid":"06426988-71dd-4da8-885b-1b63861e8ef1","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"Stop","hookName":"Stop","command":"entire hooks claude-code stop"},"parentToolUseID":"bc3b3427-b4d3-45e9-b967-2cf7c3518cc3","toolUseID":"bc3b3427-b4d3-45e9-b967-2cf7c3518cc3","timestamp":"2026-03-04T09:38:17.561Z","uuid":"43a0f2fe-58fd-42cb-b836-309ca549b1b8"} +{"parentUuid":"43a0f2fe-58fd-42cb-b836-309ca549b1b8","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"system","subtype":"stop_hook_summary","hookCount":1,"hookInfos":[{"command":"entire hooks claude-code stop","durationMs":398}],"hookErrors":[],"preventedContinuation":false,"stopReason":"","hasOutput":true,"level":"suggestion","timestamp":"2026-03-04T09:38:17.960Z","uuid":"d4bbd996-7eb3-4b5e-84eb-9ba9fbab6c25","toolUseID":"bc3b3427-b4d3-45e9-b967-2cf7c3518cc3"} +{"type":"file-history-snapshot","messageId":"e29047af-7fbb-4cfe-b5bc-59fb55133305","snapshot":{"messageId":"e29047af-7fbb-4cfe-b5bc-59fb55133305","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v3","version":3,"backupTime":"2026-03-03T09:45:34.548Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v3","version":3,"backupTime":"2026-03-04T09:35:30.335Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v2","version":2,"backupTime":"2026-03-02T19:45:28.129Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/config.yaml.example":{"backupFileName":"ab8acefcb43dda84@v2","version":2,"backupTime":"2026-03-02T19:45:28.132Z"}},"timestamp":"2026-03-04T09:41:53.652Z"},"isSnapshotUpdate":false} +{"parentUuid":"d4bbd996-7eb3-4b5e-84eb-9ba9fbab6c25","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":"decode these users, the first column is id: REDACTED|d67qqb69kmnc73b2nbm0|owner|0|0||[]|0|0|2026-02-22 14:49:17.460665591+00:00|2026-02-13 23:01:00.235815714+00:00|api|0||REDACTED|REDACTED\nREDACTED|d67qqb69kmnc73b2nbm0|admin|0|0||[]|0|0||2026-02-13 23:07:34.026260389+00:00|api|0||REDACTED|REDACTED\nREDACTED|d67qqb69kmnc73b2nbm0|admin|0|0||[\"d67qth69kmnc73b2nli0\"]|0|0||2026-02-13 23:12:13.963185087+00:00|api|0||REDACTED|REDACTED\nREDACTED|d67qqb69kmnc73b2nbm0|user|0|0||[\"d67qth69kmnc73b2nli0\"]|0|0||2026-02-13 23:39:32.97851149+00:00|api|0||REDACTED|REDACTED\nREDACTED|d67qqb69kmnc73b2nbm0|admin|0|0||[]|0|0|2026-02-19 09:44:19.187236451+00:00|2026-02-16 18:22:33.166228524+00:00|api|0||REDACTED|REDACTED"},"uuid":"e29047af-7fbb-4cfe-b5bc-59fb55133305","timestamp":"2026-03-04T09:41:52.588Z","permissionMode":"default"} +{"parentUuid":"e29047af-7fbb-4cfe-b5bc-59fb55133305","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01Ss5ttFdYAmso1QjBKcLbWA","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01QBuFtrXCfXigc2xM1HM66S","name":"Bash","input":{"command":"for encoded in \\\n \"REDACTED\" \\\n \"REDACTED\" \\\n \"REDACTED\" \\\n \"REDACTED\" \\\n \"REDACTED\"; do\n echo \"--- $encoded ---\"\n echo \"$encoded\" | base64 -d | cat -v\n echo\n echo\ndone","description":"Decode all 5 Dex user IDs"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":1279,"cache_read_input_tokens":91791,"output_tokens":547,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":1279,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhpaa1FAs2vvxcrtfEPP","type":"assistant","uuid":"9930d7f0-1107-4087-b078-5cacd5709a21","timestamp":"2026-03-04T09:42:01.184Z"} +{"parentUuid":"9930d7f0-1107-4087-b078-5cacd5709a21","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01QBuFtrXCfXigc2xM1HM66S","type":"tool_result","content":"--- REDACTED ---\n\n$c701d400-ff0c-4f87-b8ca-154a8001815d^R^Elocal\n\n--- REDACTED ---\n\n@381d44eb2438e17b375c3c82151a6fcbbcdb0b937ad6b5893f3d2deba1e99c96^R^^authentik-d67qt8m9kmnc73b2ng\n\n--- REDACTED ---\n\n$7effea22-d1cf-41ab-9a72-75ee0cf5a211^R^]pocketid-d67qvdm9kmnc73b2ntb0\n\n--- REDACTED ---\n\n$c7097de7-04cf-415e-91c2-8eed92d4a102^R^]pocketid-d67rbam9kmnc73b2o6n0\n\n--- REDACTED ---\n\n$4590aa8c-b16f-4ddf-a887-200b43361fec^R^Elocal","is_error":false}]},"uuid":"8ebd1c51-c73f-4195-b3ec-b8861482fd85","timestamp":"2026-03-04T09:42:03.699Z","toolUseResult":{"stdout":"--- REDACTED ---\n\n$c701d400-ff0c-4f87-b8ca-154a8001815d^R^Elocal\n\n--- REDACTED ---\n\n@381d44eb2438e17b375c3c82151a6fcbbcdb0b937ad6b5893f3d2deba1e99c96^R^^authentik-d67qt8m9kmnc73b2ng\n\n--- REDACTED ---\n\n$7effea22-d1cf-41ab-9a72-75ee0cf5a211^R^]pocketid-d67qvdm9kmnc73b2ntb0\n\n--- REDACTED ---\n\n$c7097de7-04cf-415e-91c2-8eed92d4a102^R^]pocketid-d67rbam9kmnc73b2o6n0\n\n--- REDACTED ---\n\n$4590aa8c-b16f-4ddf-a887-200b43361fec^R^Elocal","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"9930d7f0-1107-4087-b078-5cacd5709a21"} +{"parentUuid":"8ebd1c51-c73f-4195-b3ec-b8861482fd85","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01E4h8aqvGBUTHqVYvLXoQRk","type":"message","role":"assistant","content":[{"type":"text","text":"| # | User ID | Connector | Role |\n|---|---------|-----------|------|\n| 1 | `c701d400-ff0c-4f87-b8ca-154a8001815d` | `local` | owner |\n| 2 | `381d44eb2438e17b375c3c82151a6fcbbcdb0b937ad6b5893f3d2deba1e99c96` | `authentik-d67qt8m9kmnc73b2nggg` | admin |\n| 3 | `7effea22-d1cf-41ab-9a72-75ee0cf5a211` | `pocketid-d67qvdm9kmnc73b2ntb0` | admin |\n| 4 | `c7097de7-04cf-415e-91c2-8eed92d4a102` | `pocketid-d67rbam9kmnc73b2o6n0` | user |\n| 5 | `4590aa8c-b16f-4ddf-a887-200b43361fec` | `local` | admin |\n\nUsers 1 and 5 are from the embedded IdP (`local`), user 2 from Authentik, and users 3/4 from Pocket ID."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":1313,"cache_read_input_tokens":93070,"output_tokens":324,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":1313,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhpbKrUb7WQsQf1S3AqE","type":"assistant","uuid":"54138762-19fe-45f7-a480-b0cc109456c3","timestamp":"2026-03-04T09:42:10.729Z"} +{"parentUuid":"54138762-19fe-45f7-a480-b0cc109456c3","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"Stop","hookName":"Stop","command":"entire hooks claude-code stop"},"parentToolUseID":"eeedd781-96ac-40ec-8f30-95df1103482d","toolUseID":"eeedd781-96ac-40ec-8f30-95df1103482d","timestamp":"2026-03-04T09:42:10.736Z","uuid":"98462709-0151-4bdb-98ad-f548389aee45"} +{"parentUuid":"98462709-0151-4bdb-98ad-f548389aee45","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"system","subtype":"stop_hook_summary","hookCount":1,"hookInfos":[{"command":"entire hooks claude-code stop","durationMs":711}],"hookErrors":[],"preventedContinuation":false,"stopReason":"","hasOutput":true,"level":"suggestion","timestamp":"2026-03-04T09:42:11.453Z","uuid":"3fb09f06-fc18-42be-a396-ed8bc32ab378","toolUseID":"eeedd781-96ac-40ec-8f30-95df1103482d"} +{"type":"file-history-snapshot","messageId":"5f85df55-5c06-419e-bc44-30063a3291aa","snapshot":{"messageId":"5f85df55-5c06-419e-bc44-30063a3291aa","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v3","version":3,"backupTime":"2026-03-03T09:45:34.548Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v3","version":3,"backupTime":"2026-03-04T09:35:30.335Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v2","version":2,"backupTime":"2026-03-02T19:45:28.129Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/config.yaml.example":{"backupFileName":"ab8acefcb43dda84@v2","version":2,"backupTime":"2026-03-02T19:45:28.132Z"}},"timestamp":"2026-03-04T09:43:00.561Z"},"isSnapshotUpdate":false} +{"parentUuid":"3fb09f06-fc18-42be-a396-ed8bc32ab378","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":"add the original stored id "},"uuid":"5f85df55-5c06-419e-bc44-30063a3291aa","timestamp":"2026-03-04T09:42:59.596Z","permissionMode":"default"} +{"parentUuid":"5f85df55-5c06-419e-bc44-30063a3291aa","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_018YyaTGSvW1ffuWgV2kJjGm","type":"message","role":"assistant","content":[{"type":"text","text":"\n\n| # | Stored ID | User ID | Connector |Role |\n|---|-----------|---------|-----------|------|\n| 1 | `REDACTED` | `c701d400-ff0c-4f87-b8ca-154a8001815d` | `local` | owner |\n| 2 | `REDACTED` | `381d44eb2438e17b375c3c82151a6fcbbcdb0b937ad6b5893f3d2deba1e99c96` | `authentik-d67qt8m9kmnc73b2nggg` | admin |\n| 3 | `REDACTED` | `7effea22-d1cf-41ab-9a72-75ee0cf5a211` | `pocketid-d67qvdm9kmnc73b2ntb0` | admin |\n| 4 | `REDACTED` | `c7097de7-04cf-415e-91c2-8eed92d4a102` | `pocketid-d67rbam9kmnc73b2o6n0` | user |\n| 5 | `REDACTED` | `4590aa8c-b16f-4ddf-a887-200b43361fec` | `local` | admin |"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":9,"cache_read_input_tokens":94705,"output_tokens":711,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":9,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhpfWa1V6iknxbAnzxaz","type":"assistant","uuid":"643fca99-926b-4f0d-90bf-1e1c96d5d1b6","timestamp":"2026-03-04T09:43:09.155Z"} +{"parentUuid":"643fca99-926b-4f0d-90bf-1e1c96d5d1b6","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"Stop","hookName":"Stop","command":"entire hooks claude-code stop"},"parentToolUseID":"d855cc66-ceff-4217-86c7-c0a61255336a","toolUseID":"d855cc66-ceff-4217-86c7-c0a61255336a","timestamp":"2026-03-04T09:43:09.186Z","uuid":"a71997c8-7cbc-46a4-9d3c-7c55f6a6b64a"} +{"parentUuid":"a71997c8-7cbc-46a4-9d3c-7c55f6a6b64a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"system","subtype":"stop_hook_summary","hookCount":1,"hookInfos":[{"command":"entire hooks claude-code stop","durationMs":384}],"hookErrors":[],"preventedContinuation":false,"stopReason":"","hasOutput":true,"level":"suggestion","timestamp":"2026-03-04T09:43:09.571Z","uuid":"17cb9257-f327-478a-928b-56d2eb71609e","toolUseID":"d855cc66-ceff-4217-86c7-c0a61255336a"} +{"type":"file-history-snapshot","messageId":"1295fe4c-9944-4962-8a8f-cf1ba56b6d32","snapshot":{"messageId":"1295fe4c-9944-4962-8a8f-cf1ba56b6d32","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v3","version":3,"backupTime":"2026-03-03T09:45:34.548Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v3","version":3,"backupTime":"2026-03-04T09:35:30.335Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v2","version":2,"backupTime":"2026-03-02T19:45:28.129Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/config.yaml.example":{"backupFileName":"ab8acefcb43dda84@v2","version":2,"backupTime":"2026-03-02T19:45:28.132Z"}},"timestamp":"2026-03-04T09:43:26.705Z"},"isSnapshotUpdate":false} +{"parentUuid":"17cb9257-f327-478a-928b-56d2eb71609e","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":"add a column after decoding base64"},"uuid":"1295fe4c-9944-4962-8a8f-cf1ba56b6d32","timestamp":"2026-03-04T09:43:25.808Z","permissionMode":"default"} +{"parentUuid":"1295fe4c-9944-4962-8a8f-cf1ba56b6d32","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01WWmZWLPJ226BqbPfdFam18","type":"message","role":"assistant","content":[{"type":"text","text":"\n\n| # | Stored ID (base64) | Decoded (raw) | User ID | Connector | Role |\n|---|-----------|---------------|---------|-----------|------|\n| 1 | `REDACTED` | `\\x0a$c701d400-ff0c-4f87-b8ca-154a8001815d\\x12\\x05local` | `c701d400-ff0c-4f87-b8ca-154a8001815d` | `local` | owner |\n| 2 | `REDACTED` | `\\x0a@381d44eb2438e17b375c3c82151a6fcbbcdb0b937ad6b5893f3d2deba1e99c96\\x12\\x1eauthentik-d67qt8m9kmnc73b2nggg` | `381d44eb2438e17b375c3c82151a6fcbbcdb0b937ad6b5893f3d2deba1e99c96` | `authentik-d67qt8m9kmnc73b2nggg` | admin |\n| 3 | `REDACTED` | `\\x0a$7effea22-d1cf-41ab-9a72-75ee0cf5a211\\x12\\x1dpocketid-d67qvdm9kmnc73b2ntb0` | `7effea22-d1cf-41ab-9a72-75ee0cf5a211` | `pocketid-d67qvdm9kmnc73b2ntb0` | admin |\n| 4 | `REDACTED` | `\\x0a$c7097de7-04cf-415e-91c2-8eed92d4a102\\x12\\x1dpocketid-d67rbam9kmnc73b2o6n0` | `c7097de7-04cf-415e-91c2-8eed92d4a102` | `pocketid-d67rbam9kmnc73b2o6n0` | user |\n| 5 | `REDACTED` | `\\x0a$4590aa8c-b16f-4ddf-a887-200b43361fec\\x12\\x05local` | `4590aa8c-b16f-4ddf-a887-200b43361fec` | `local` | admin |\n\nThe \"Decoded\" column shows the raw protobuf bytes — `\\x0a` is field 1 tag, followed by a varint length, then the user ID string; `\\x12` is field 2 tag, followed by length and connector string."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":11,"cache_read_input_tokens":95426,"output_tokens":1047,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":11,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhphRvWgGaA5YkvWemu2","type":"assistant","uuid":"b5f3c5d7-d21c-408e-aea6-57d04b6b01f3","timestamp":"2026-03-04T09:43:39.106Z"} +{"parentUuid":"b5f3c5d7-d21c-408e-aea6-57d04b6b01f3","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"Stop","hookName":"Stop","command":"entire hooks claude-code stop"},"parentToolUseID":"327d72f5-9e0d-4394-9764-cf5f974ba166","toolUseID":"327d72f5-9e0d-4394-9764-cf5f974ba166","timestamp":"2026-03-04T09:43:39.142Z","uuid":"eca691d9-c8e1-448a-8d15-ae1eca5825b3"} +{"parentUuid":"eca691d9-c8e1-448a-8d15-ae1eca5825b3","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"system","subtype":"stop_hook_summary","hookCount":1,"hookInfos":[{"command":"entire hooks claude-code stop","durationMs":664}],"hookErrors":[],"preventedContinuation":false,"stopReason":"","hasOutput":true,"level":"suggestion","timestamp":"2026-03-04T09:43:39.807Z","uuid":"b8bf8f95-b9b5-4e5e-893e-d40a43a7c7c8","toolUseID":"327d72f5-9e0d-4394-9764-cf5f974ba166"} +{"type":"file-history-snapshot","messageId":"af2b1ed5-da44-4083-a356-f7edb52c68b8","snapshot":{"messageId":"af2b1ed5-da44-4083-a356-f7edb52c68b8","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v3","version":3,"backupTime":"2026-03-03T09:45:34.548Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v3","version":3,"backupTime":"2026-03-04T09:35:30.335Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v2","version":2,"backupTime":"2026-03-02T19:45:28.129Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/config.yaml.example":{"backupFileName":"ab8acefcb43dda84@v2","version":2,"backupTime":"2026-03-02T19:45:28.132Z"}},"timestamp":"2026-03-04T09:45:12.815Z"},"isSnapshotUpdate":false} +{"parentUuid":"b8bf8f95-b9b5-4e5e-893e-d40a43a7c7c8","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":"in the // Returns the type prefix, or \"oidc\" if no known prefix is found.\nfunc extractIdpType(connectorID string) string {\n idx := strings.LastIndex(connectorID, \"-\")\n if idx <= 0 {\n return \"oidc\"\n }\n return strings.ToLower(connectorID[:idx])\n} in management/server/metrics/selfhosted.go I think that we don't count local or maybe counting it wrong. Could you please check it, fix it and add a test?"},"uuid":"af2b1ed5-da44-4083-a356-f7edb52c68b8","timestamp":"2026-03-04T09:45:11.848Z","permissionMode":"default"} +{"parentUuid":"af2b1ed5-da44-4083-a356-f7edb52c68b8","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01HQFV3g8sripoapgbwpRAD9","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01TRQNH4NZRWafpA7DmLC7xF","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":284,"cache_read_input_tokens":96485,"output_tokens":83,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":284,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhpqFbPDtQWZCKnSrWnj","type":"assistant","uuid":"498fc62d-db9e-484f-9657-3c95e844bb50","timestamp":"2026-03-04T09:45:16.950Z"} +{"parentUuid":"498fc62d-db9e-484f-9657-3c95e844bb50","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01TRQNH4NZRWafpA7DmLC7xF","toolUseID":"toolu_01TRQNH4NZRWafpA7DmLC7xF","timestamp":"2026-03-04T09:45:17.065Z","uuid":"502ecefc-ab7d-4281-94f6-d0e5fb3784c7"} +{"parentUuid":"498fc62d-db9e-484f-9657-3c95e844bb50","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01TRQNH4NZRWafpA7DmLC7xF","type":"tool_result","content":" 1→// Package metrics gather anonymous information about the usage of NetBird management\n 2→package metrics\n 3→\n 4→import (\n 5→\t\"context\"\n 6→\t\"encoding/json\"\n 7→\t\"fmt\"\n 8→\t\"io\"\n 9→\t\"net/http\"\n 10→\t\"os\"\n 11→\t\"sort\"\n 12→\t\"strings\"\n 13→\t\"time\"\n 14→\n 15→\t\"github.com/hashicorp/go-version\"\n 16→\t\"github.com/netbirdio/netbird/idp/dex\"\n 17→\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n 18→\tlog \"github.com/sirupsen/logrus\"\n 19→\n 20→\t\"github.com/netbirdio/netbird/management/server/types\"\n 21→\tnbversion \"github.com/netbirdio/netbird/version\"\n 22→)\n 23→\n 24→const (\n 25→\t// PayloadEvent identifies an event type\n 26→\tPayloadEvent = \"self-hosted stats\"\n 27→\t// payloadEndpoint metrics defaultEndpoint to send anonymous data\n 28→\tpayloadEndpoint = \"https://metrics.netbird.io\"\n 29→\t// defaultPushInterval default interval to push metrics\n 30→\tdefaultPushInterval = 12 * time.Hour\n 31→\t// requestTimeout http request timeout\n 32→\trequestTimeout = 45 * time.Second\n 33→\tEmbeddedType = \"embedded\"\n 34→)\n 35→\n 36→type getTokenResponse struct {\n 37→\tPublicAPIToken string `json:\"public_api_token\"`\n 38→}\n 39→\n 40→type pushPayload struct {\n 41→\tAPIKey string `json:\"api_key\"`\n 42→\tDistinctID string `json:\"distinct_id\"`\n 43→\tEvent string `json:\"event\"`\n 44→\tProperties properties `json:\"properties\"`\n 45→\tTimestamp time.Time `json:\"timestamp\"`\n 46→}\n 47→\n 48→// properties metrics to push\n 49→type properties map[string]interface{}\n 50→\n 51→// DataSource metric data source\n 52→type DataSource interface {\n 53→\tGetAllAccounts(ctx context.Context) []*types.Account\n 54→\tGetStoreEngine() types.Engine\n 55→\tGetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)\n 56→}\n 57→\n 58→// ConnManager peer connection manager that holds state for current active connections\n 59→type ConnManager interface {\n 60→\tGetAllConnectedPeers() map[string]struct{}\n 61→}\n 62→\n 63→// Worker metrics collector and pusher\n 64→type Worker struct {\n 65→\tctx context.Context\n 66→\tid string\n 67→\tidpManager string\n 68→\tdataSource DataSource\n 69→\tconnManager ConnManager\n 70→\tstartupTime time.Time\n 71→\tlastRun time.Time\n 72→}\n 73→\n 74→// NewWorker returns a metrics worker\n 75→func NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {\n 76→\tcurrentTime := time.Now()\n 77→\treturn &Worker{\n 78→\t\tctx: ctx,\n 79→\t\tid: id,\n 80→\t\tidpManager: idpManager,\n 81→\t\tdataSource: dataSource,\n 82→\t\tconnManager: connManager,\n 83→\t\tstartupTime: currentTime,\n 84→\t\tlastRun: currentTime,\n 85→\t}\n 86→}\n 87→\n 88→// Run runs the metrics worker\n 89→func (w *Worker) Run(ctx context.Context) {\n 90→\tinterval := getMetricsInterval(ctx)\n 91→\n 92→\tpushTicker := time.NewTicker(interval)\n 93→\tfor {\n 94→\t\tselect {\n 95→\t\tcase <-w.ctx.Done():\n 96→\t\t\treturn\n 97→\t\tcase <-pushTicker.C:\n 98→\t\t\terr := w.sendMetrics(ctx)\n 99→\t\t\tif err != nil {\n 100→\t\t\t\tlog.WithContext(ctx).Error(err)\n 101→\t\t\t}\n 102→\t\t\tw.lastRun = time.Now()\n 103→\t\t}\n 104→\t}\n 105→}\n 106→\n 107→func getMetricsInterval(ctx context.Context) time.Duration {\n 108→\tinterval := defaultPushInterval\n 109→\tif os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") != \"\" {\n 110→\t\tnewInterval, err := time.ParseDuration(os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") + \"s\")\n 111→\t\tif err != nil {\n 112→\t\t\tlog.WithContext(ctx).Errorf(\"unable to parse NETBIRD_METRICS_INTERVAL_IN_SECONDS, using default interval %v. Error: %v\", defaultPushInterval, err)\n 113→\t\t} else {\n 114→\t\t\tlog.WithContext(ctx).Infof(\"using NETBIRD_METRICS_INTERVAL_IN_SECONDS %s\", newInterval)\n 115→\t\t\tinterval = newInterval\n 116→\t\t}\n 117→\t}\n 118→\treturn interval\n 119→}\n 120→\n 121→func (w *Worker) sendMetrics(ctx context.Context) error {\n 122→\tapiKey, err := getAPIKey(w.ctx)\n 123→\tif err != nil {\n 124→\t\treturn err\n 125→\t}\n 126→\n 127→\tpayload := w.generatePayload(ctx, apiKey)\n 128→\n 129→\tpayloadString, err := buildMetricsPayload(payload)\n 130→\tif err != nil {\n 131→\t\treturn err\n 132→\t}\n 133→\n 134→\thttpClient := http.Client{}\n 135→\n 136→\texportJobReq, cancelCTX, err := createPostRequest(w.ctx, payloadEndpoint+\"/capture/\", payloadString)\n 137→\tif err != nil {\n 138→\t\treturn fmt.Errorf(\"unable to create metrics post request %v\", err)\n 139→\t}\n 140→\tdefer cancelCTX()\n 141→\n 142→\tjobResp, err := httpClient.Do(exportJobReq)\n 143→\tif err != nil {\n 144→\t\treturn fmt.Errorf(\"unable to push metrics %v\", err)\n 145→\t}\n 146→\n 147→\tdefer func() {\n 148→\t\terr = jobResp.Body.Close()\n 149→\t\tif err != nil {\n 150→\t\t\tlog.WithContext(ctx).Errorf(\"error while closing update metrics response body: %v\", err)\n 151→\t\t}\n 152→\t}()\n 153→\n 154→\tif jobResp.StatusCode != 200 {\n 155→\t\treturn fmt.Errorf(\"unable to push anonymous metrics, got statusCode %d\", jobResp.StatusCode)\n 156→\t}\n 157→\n 158→\tlog.WithContext(ctx).Infof(\"sent anonymous metrics, next push will happen in %s. \"+\n 159→\t\t\"You can disable these metrics by running with flag --disable-anonymous-metrics,\"+\n 160→\t\t\" see more information at https://docs.netbird.io/about-netbird/faq#why-and-what-are-the-anonymous-usage-metrics\", getMetricsInterval(ctx))\n 161→\n 162→\treturn nil\n 163→}\n 164→\n 165→func (w *Worker) generatePayload(ctx context.Context, apiKey string) pushPayload {\n 166→\tproperties := w.generateProperties(ctx)\n 167→\n 168→\treturn pushPayload{\n 169→\t\tAPIKey: apiKey,\n 170→\t\tDistinctID: w.id,\n 171→\t\tEvent: PayloadEvent,\n 172→\t\tProperties: properties,\n 173→\t\tTimestamp: time.Now(),\n 174→\t}\n 175→}\n 176→\n 177→func (w *Worker) generateProperties(ctx context.Context) properties {\n 178→\tvar (\n 179→\t\tuptime float64\n 180→\t\taccounts int\n 181→\t\texpirationEnabled int\n 182→\t\tusers int\n 183→\t\tserviceUsers int\n 184→\t\tpats int\n 185→\t\tpeers int\n 186→\t\tpeersSSHEnabled int\n 187→\t\tsetupKeysUsage int\n 188→\t\tephemeralPeersSKs int\n 189→\t\tephemeralPeersSKUsage int\n 190→\t\tactivePeersLastDay int\n 191→\t\tactiveUserPeersLastDay int\n 192→\t\tosPeers map[string]int\n 193→\t\tactiveUsersLastDay map[string]struct{}\n 194→\t\tuserPeers int\n 195→\t\trules int\n 196→\t\trulesProtocol map[string]int\n 197→\t\trulesDirection map[string]int\n 198→\t\trulesWithSrcPostureChecks int\n 199→\t\tpostureChecks int\n 200→\t\tgroups int\n 201→\t\troutes int\n 202→\t\troutesWithRGGroups int\n 203→\t\tnetworks int\n 204→\t\tnetworkResources int\n 205→\t\tnetworkRouters int\n 206→\t\tnetworkRoutersWithPG int\n 207→\t\tnameservers int\n 208→\t\tuiClient int\n 209→\t\tversion string\n 210→\t\tpeerActiveVersions []string\n 211→\t\tosUIClients map[string]int\n 212→\t\trosenpassEnabled int\n 213→\t\tlocalUsers int\n 214→\t\tidpUsers int\n 215→\t\tembeddedIdpTypes map[string]int\n 216→\t\tservices int\n 217→\t\tservicesEnabled int\n 218→\t\tservicesTargets int\n 219→\t\tservicesStatusActive int\n 220→\t\tservicesStatusPending int\n 221→\t\tservicesStatusError int\n 222→\t\tservicesTargetType map[string]int\n 223→\t\tservicesAuthPassword int\n 224→\t\tservicesAuthPin int\n 225→\t\tservicesAuthOIDC int\n 226→\t)\n 227→\tstart := time.Now()\n 228→\tmetricsProperties := make(properties)\n 229→\tosPeers = make(map[string]int)\n 230→\tosUIClients = make(map[string]int)\n 231→\trulesProtocol = make(map[string]int)\n 232→\trulesDirection = make(map[string]int)\n 233→\tactiveUsersLastDay = make(map[string]struct{})\n 234→\tembeddedIdpTypes = make(map[string]int)\n 235→\tservicesTargetType = make(map[string]int)\n 236→\tuptime = time.Since(w.startupTime).Seconds()\n 237→\tconnections := w.connManager.GetAllConnectedPeers()\n 238→\tversion = nbversion.NetbirdVersion()\n 239→\n 240→\tcustomDomains, customDomainsValidated, _ := w.dataSource.GetCustomDomainsCounts(ctx)\n 241→\n 242→\tfor _, account := range w.dataSource.GetAllAccounts(ctx) {\n 243→\t\taccounts++\n 244→\n 245→\t\tif account.Settings.PeerLoginExpirationEnabled {\n 246→\t\t\texpirationEnabled++\n 247→\t\t}\n 248→\n 249→\t\tgroups += len(account.Groups)\n 250→\t\tnetworks += len(account.Networks)\n 251→\t\tnetworkResources += len(account.NetworkResources)\n 252→\n 253→\t\tnetworkRouters += len(account.NetworkRouters)\n 254→\t\tfor _, router := range account.NetworkRouters {\n 255→\t\t\tif len(router.PeerGroups) > 0 {\n 256→\t\t\t\tnetworkRoutersWithPG++\n 257→\t\t\t}\n 258→\t\t}\n 259→\n 260→\t\troutes += len(account.Routes)\n 261→\t\tfor _, route := range account.Routes {\n 262→\t\t\tif len(route.PeerGroups) > 0 {\n 263→\t\t\t\troutesWithRGGroups++\n 264→\t\t\t}\n 265→\t\t}\n 266→\t\tnameservers += len(account.NameServerGroups)\n 267→\n 268→\t\tfor _, policy := range account.Policies {\n 269→\t\t\tfor _, rule := range policy.Rules {\n 270→\t\t\t\trules++\n 271→\t\t\t\trulesProtocol[string(rule.Protocol)]++\n 272→\t\t\t\tif rule.Bidirectional {\n 273→\t\t\t\t\trulesDirection[\"bidirectional\"]++\n 274→\t\t\t\t} else {\n 275→\t\t\t\t\trulesDirection[\"oneway\"]++\n 276→\t\t\t\t}\n 277→\t\t\t}\n 278→\t\t\tif len(policy.SourcePostureChecks) > 0 {\n 279→\t\t\t\trulesWithSrcPostureChecks++\n 280→\t\t\t}\n 281→\t\t}\n 282→\n 283→\t\tpostureChecks += len(account.PostureChecks)\n 284→\n 285→\t\tfor _, user := range account.Users {\n 286→\t\t\tif user.IsServiceUser {\n 287→\t\t\t\tserviceUsers++\n 288→\t\t\t} else {\n 289→\t\t\t\tusers++\n 290→\t\t\t\tif w.idpManager == EmbeddedType {\n 291→\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n 292→\t\t\t\t\tif err == nil {\n 293→\t\t\t\t\t\tif idpID == \"local\" {\n 294→\t\t\t\t\t\t\tlocalUsers++\n 295→\t\t\t\t\t\t} else {\n 296→\t\t\t\t\t\t\tidpUsers++\n 297→\t\t\t\t\t\t\tidpType := extractIdpType(idpID)\n 298→\t\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n 299→\t\t\t\t\t\t}\n 300→\t\t\t\t\t}\n 301→\t\t\t\t}\n 302→\t\t\t}\n 303→\t\t\tpats += len(user.PATs)\n 304→\t\t}\n 305→\n 306→\t\tfor _, key := range account.SetupKeys {\n 307→\t\t\tsetupKeysUsage += key.UsedTimes\n 308→\t\t\tif key.Ephemeral {\n 309→\t\t\t\tephemeralPeersSKs++\n 310→\t\t\t\tephemeralPeersSKUsage += key.UsedTimes\n 311→\t\t\t}\n 312→\t\t}\n 313→\n 314→\t\tfor _, peer := range account.Peers {\n 315→\t\t\tpeers++\n 316→\n 317→\t\t\tif peer.SSHEnabled || peer.Meta.Flags.ServerSSHAllowed {\n 318→\t\t\t\tpeersSSHEnabled++\n 319→\t\t\t}\n 320→\n 321→\t\t\tif peer.Meta.Flags.RosenpassEnabled {\n 322→\t\t\t\trosenpassEnabled++\n 323→\t\t\t}\n 324→\n 325→\t\t\tif peer.UserID != \"\" {\n 326→\t\t\t\tuserPeers++\n 327→\t\t\t}\n 328→\n 329→\t\t\tosKey := strings.ToLower(fmt.Sprintf(\"peer_os_%s\", peer.Meta.GoOS))\n 330→\t\t\tosCount := osPeers[osKey]\n 331→\t\t\tosPeers[osKey] = osCount + 1\n 332→\n 333→\t\t\tif peer.Meta.UIVersion != \"\" {\n 334→\t\t\t\tuiClient++\n 335→\t\t\t\tuiOSKey := strings.ToLower(fmt.Sprintf(\"ui_client_os_%s\", peer.Meta.GoOS))\n 336→\t\t\t\tosUICount := osUIClients[uiOSKey]\n 337→\t\t\t\tosUIClients[uiOSKey] = osUICount + 1\n 338→\t\t\t}\n 339→\n 340→\t\t\t_, connected := connections[peer.ID]\n 341→\t\t\tif connected || peer.Status.LastSeen.After(w.lastRun) {\n 342→\t\t\t\tactivePeersLastDay++\n 343→\t\t\t\tif peer.UserID != \"\" {\n 344→\t\t\t\t\tactiveUserPeersLastDay++\n 345→\t\t\t\t\tactiveUsersLastDay[peer.UserID] = struct{}{}\n 346→\t\t\t\t}\n 347→\t\t\t\tosActiveKey := osKey + \"_active\"\n 348→\t\t\t\tosActiveCount := osPeers[osActiveKey]\n 349→\t\t\t\tosPeers[osActiveKey] = osActiveCount + 1\n 350→\t\t\t\tpeerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)\n 351→\t\t\t}\n 352→\t\t}\n 353→\n 354→\t\tfor _, service := range account.Services {\n 355→\t\t\tservices++\n 356→\t\t\tif service.Enabled {\n 357→\t\t\t\tservicesEnabled++\n 358→\t\t\t}\n 359→\t\t\tservicesTargets += len(service.Targets)\n 360→\n 361→\t\t\tswitch rpservice.Status(service.Meta.Status) {\n 362→\t\t\tcase rpservice.StatusActive:\n 363→\t\t\t\tservicesStatusActive++\n 364→\t\t\tcase rpservice.StatusPending:\n 365→\t\t\t\tservicesStatusPending++\n 366→\t\t\tcase rpservice.StatusError, rpservice.StatusCertificateFailed, rpservice.StatusTunnelNotCreated:\n 367→\t\t\t\tservicesStatusError++\n 368→\t\t\t}\n 369→\n 370→\t\t\tfor _, target := range service.Targets {\n 371→\t\t\t\tservicesTargetType[target.TargetType]++\n 372→\t\t\t}\n 373→\n 374→\t\t\tif service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled {\n 375→\t\t\t\tservicesAuthPassword++\n 376→\t\t\t}\n 377→\t\t\tif service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled {\n 378→\t\t\t\tservicesAuthPin++\n 379→\t\t\t}\n 380→\t\t\tif service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled {\n 381→\t\t\t\tservicesAuthOIDC++\n 382→\t\t\t}\n 383→\t\t}\n 384→\t}\n 385→\n 386→\tminActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)\n 387→\tmetricsProperties[\"uptime\"] = uptime\n 388→\tmetricsProperties[\"accounts\"] = accounts\n 389→\tmetricsProperties[\"users\"] = users\n 390→\tmetricsProperties[\"service_users\"] = serviceUsers\n 391→\tmetricsProperties[\"pats\"] = pats\n 392→\tmetricsProperties[\"peers\"] = peers\n 393→\tmetricsProperties[\"peers_ssh_enabled\"] = peersSSHEnabled\n 394→\tmetricsProperties[\"peers_login_expiration_enabled\"] = expirationEnabled\n 395→\tmetricsProperties[\"setup_keys_usage\"] = setupKeysUsage\n 396→\tmetricsProperties[\"ephemeral_peers_setup_keys\"] = ephemeralPeersSKs\n 397→\tmetricsProperties[\"ephemeral_peers_setup_keys_usage\"] = ephemeralPeersSKUsage\n 398→\tmetricsProperties[\"active_peers_last_day\"] = activePeersLastDay\n 399→\tmetricsProperties[\"active_user_peers_last_day\"] = activeUserPeersLastDay\n 400→\tmetricsProperties[\"active_users_last_day\"] = len(activeUsersLastDay)\n 401→\tmetricsProperties[\"user_peers\"] = userPeers\n 402→\tmetricsProperties[\"rules\"] = rules\n 403→\tmetricsProperties[\"rules_with_src_posture_checks\"] = rulesWithSrcPostureChecks\n 404→\tmetricsProperties[\"posture_checks\"] = postureChecks\n 405→\tmetricsProperties[\"groups\"] = groups\n 406→\tmetricsProperties[\"networks\"] = networks\n 407→\tmetricsProperties[\"network_resources\"] = networkResources\n 408→\tmetricsProperties[\"network_routers\"] = networkRouters\n 409→\tmetricsProperties[\"network_routers_with_groups\"] = networkRoutersWithPG\n 410→\tmetricsProperties[\"routes\"] = routes\n 411→\tmetricsProperties[\"routes_with_routing_groups\"] = routesWithRGGroups\n 412→\tmetricsProperties[\"nameservers\"] = nameservers\n 413→\tmetricsProperties[\"version\"] = version\n 414→\tmetricsProperties[\"min_active_peer_version\"] = minActivePeerVersion\n 415→\tmetricsProperties[\"max_active_peer_version\"] = maxActivePeerVersion\n 416→\tmetricsProperties[\"ui_clients\"] = uiClient\n 417→\tmetricsProperties[\"idp_manager\"] = w.idpManager\n 418→\tmetricsProperties[\"store_engine\"] = w.dataSource.GetStoreEngine()\n 419→\tmetricsProperties[\"rosenpass_enabled\"] = rosenpassEnabled\n 420→\tmetricsProperties[\"local_users_count\"] = localUsers\n 421→\tmetricsProperties[\"idp_users_count\"] = idpUsers\n 422→\tmetricsProperties[\"embedded_idp_count\"] = len(embeddedIdpTypes)\n 423→\n 424→\tmetricsProperties[\"services\"] = services\n 425→\tmetricsProperties[\"services_enabled\"] = servicesEnabled\n 426→\tmetricsProperties[\"services_targets\"] = servicesTargets\n 427→\tmetricsProperties[\"services_status_active\"] = servicesStatusActive\n 428→\tmetricsProperties[\"services_status_pending\"] = servicesStatusPending\n 429→\tmetricsProperties[\"services_status_error\"] = servicesStatusError\n 430→\tmetricsProperties[\"services_auth_password\"] = servicesAuthPassword\n 431→\tmetricsProperties[\"services_auth_pin\"] = servicesAuthPin\n 432→\tmetricsProperties[\"services_auth_oidc\"] = servicesAuthOIDC\n 433→\tmetricsProperties[\"custom_domains\"] = customDomains\n 434→\tmetricsProperties[\"custom_domains_validated\"] = customDomainsValidated\n 435→\n 436→\tfor targetType, count := range servicesTargetType {\n 437→\t\tmetricsProperties[\"services_target_type_\"+targetType] = count\n 438→\t}\n 439→\n 440→\tfor idpType, count := range embeddedIdpTypes {\n 441→\t\tmetricsProperties[\"embedded_idp_users_\"+idpType] = count\n 442→\t}\n 443→\n 444→\tfor protocol, count := range rulesProtocol {\n 445→\t\tmetricsProperties[\"rules_protocol_\"+protocol] = count\n 446→\t}\n 447→\n 448→\tfor direction, count := range rulesDirection {\n 449→\t\tmetricsProperties[\"rules_direction_\"+direction] = count\n 450→\t}\n 451→\n 452→\tfor os, count := range osPeers {\n 453→\t\tmetricsProperties[os] = count\n 454→\t}\n 455→\n 456→\tfor os, count := range osUIClients {\n 457→\t\tmetricsProperties[os] = count\n 458→\t}\n 459→\n 460→\tmetricsProperties[\"metric_generation_time\"] = time.Since(start).Milliseconds()\n 461→\n 462→\treturn metricsProperties\n 463→}\n 464→\n 465→func getAPIKey(ctx context.Context) (string, error) {\n 466→\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n 467→\tdefer cancel()\n 468→\n 469→\thttpClient := http.Client{}\n 470→\n 471→\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+\"/GetToken\", nil)\n 472→\tif err != nil {\n 473→\t\treturn \"\", fmt.Errorf(\"unable to create request for metrics public api token %v\", err)\n 474→\t}\n 475→\tresponse, err := httpClient.Do(req)\n 476→\tif err != nil {\n 477→\t\treturn \"\", fmt.Errorf(\"unable to request metrics public api token %v\", err)\n 478→\t}\n 479→\n 480→\tdefer func() {\n 481→\t\terr = response.Body.Close()\n 482→\t\tif err != nil {\n 483→\t\t\tlog.WithContext(ctx).Errorf(\"error while closing metrics token response body: %v\", err)\n 484→\t\t}\n 485→\t}()\n 486→\n 487→\tif response.StatusCode != 200 {\n 488→\t\treturn \"\", fmt.Errorf(\"unable to retrieve metrics token, statusCode %d\", response.StatusCode)\n 489→\t}\n 490→\n 491→\tbody, err := io.ReadAll(response.Body)\n 492→\tif err != nil {\n 493→\t\treturn \"\", fmt.Errorf(\"coudln't get metrics token response; %v\", err)\n 494→\t}\n 495→\n 496→\tvar tokenResponse getTokenResponse\n 497→\n 498→\terr = json.Unmarshal(body, &tokenResponse)\n 499→\tif err != nil {\n 500→\t\treturn \"\", fmt.Errorf(\"coudln't parse metrics public api token; %v\", err)\n 501→\t}\n 502→\n 503→\treturn tokenResponse.PublicAPIToken, nil\n 504→}\n 505→\n 506→func buildMetricsPayload(payload pushPayload) (string, error) {\n 507→\tstr, err := json.Marshal(payload)\n 508→\tif err != nil {\n 509→\t\treturn \"\", fmt.Errorf(\"unable to marshal metrics payload, got err: %v\", err)\n 510→\t}\n 511→\treturn string(str), nil\n 512→}\n 513→\n 514→func createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, context.CancelFunc, error) {\n 515→\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n 516→\treqURL := endpoint\n 517→\n 518→\tpayload := strings.NewReader(payloadStr)\n 519→\n 520→\treq, err := http.NewRequestWithContext(ctx, \"POST\", reqURL, payload)\n 521→\tif err != nil {\n 522→\t\tcancel()\n 523→\t\treturn nil, nil, err\n 524→\t}\n 525→\treq.Header.Add(\"content-type\", \"application/json\")\n 526→\n 527→\treturn req, cancel, nil\n 528→}\n 529→\n 530→// extractIdpType extracts the IdP type from a Dex connector ID.\n 531→// Connector IDs are formatted as \"- \" (e.g., \"okta-abc123\", \"zitadel-xyz\").\n 532→// Returns the type prefix, or \"oidc\" if no known prefix is found.\n 533→func extractIdpType(connectorID string) string {\n 534→\tidx := strings.LastIndex(connectorID, \"-\")\n 535→\tif idx <= 0 {\n 536→\t\treturn \"oidc\"\n 537→\t}\n 538→\treturn strings.ToLower(connectorID[:idx])\n 539→}\n 540→\n 541→func getMinMaxVersion(inputList []string) (string, string) {\n 542→\tversions := make([]*version.Version, 0)\n 543→\n 544→\tfor _, raw := range inputList {\n 545→\t\tif raw != \"\" && nbversion.SemverRegexp.MatchString(raw) {\n 546→\t\t\tv, err := version.NewVersion(raw)\n 547→\t\t\tif err == nil {\n 548→\t\t\t\tversions = append(versions, v)\n 549→\t\t\t}\n 550→\t\t}\n 551→\t}\n 552→\n 553→\ttargetIndex := 1\n 554→\tl := len(versions)\n 555→\n 556→\tswitch l {\n 557→\tcase 0:\n 558→\t\treturn \"\", \"\"\n 559→\tcase targetIndex:\n 560→\t\tv := versions[targetIndex-1].String()\n 561→\t\treturn v, v\n 562→\tdefault:\n 563→\t\tsort.Sort(version.Collection(versions))\n 564→\t\treturn versions[targetIndex-1].String(), versions[l-1].String()\n 565→\t}\n 566→}\n 567→"}]},"uuid":"0118fc31-4a88-4eae-9e62-87a094865a2f","timestamp":"2026-03-04T09:45:17.064Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go","content":"// Package metrics gather anonymous information about the usage of NetBird management\npackage metrics\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\tnbversion \"github.com/netbirdio/netbird/version\"\n)\n\nconst (\n\t// PayloadEvent identifies an event type\n\tPayloadEvent = \"self-hosted stats\"\n\t// payloadEndpoint metrics defaultEndpoint to send anonymous data\n\tpayloadEndpoint = \"https://metrics.netbird.io\"\n\t// defaultPushInterval default interval to push metrics\n\tdefaultPushInterval = 12 * time.Hour\n\t// requestTimeout http request timeout\n\trequestTimeout = 45 * time.Second\n\tEmbeddedType = \"embedded\"\n)\n\ntype getTokenResponse struct {\n\tPublicAPIToken string `json:\"public_api_token\"`\n}\n\ntype pushPayload struct {\n\tAPIKey string `json:\"api_key\"`\n\tDistinctID string `json:\"distinct_id\"`\n\tEvent string `json:\"event\"`\n\tProperties properties `json:\"properties\"`\n\tTimestamp time.Time `json:\"timestamp\"`\n}\n\n// properties metrics to push\ntype properties map[string]interface{}\n\n// DataSource metric data source\ntype DataSource interface {\n\tGetAllAccounts(ctx context.Context) []*types.Account\n\tGetStoreEngine() types.Engine\n\tGetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)\n}\n\n// ConnManager peer connection manager that holds state for current active connections\ntype ConnManager interface {\n\tGetAllConnectedPeers() map[string]struct{}\n}\n\n// Worker metrics collector and pusher\ntype Worker struct {\n\tctx context.Context\n\tid string\n\tidpManager string\n\tdataSource DataSource\n\tconnManager ConnManager\n\tstartupTime time.Time\n\tlastRun time.Time\n}\n\n// NewWorker returns a metrics worker\nfunc NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {\n\tcurrentTime := time.Now()\n\treturn &Worker{\n\t\tctx: ctx,\n\t\tid: id,\n\t\tidpManager: idpManager,\n\t\tdataSource: dataSource,\n\t\tconnManager: connManager,\n\t\tstartupTime: currentTime,\n\t\tlastRun: currentTime,\n\t}\n}\n\n// Run runs the metrics worker\nfunc (w *Worker) Run(ctx context.Context) {\n\tinterval := getMetricsInterval(ctx)\n\n\tpushTicker := time.NewTicker(interval)\n\tfor {\n\t\tselect {\n\t\tcase <-w.ctx.Done():\n\t\t\treturn\n\t\tcase <-pushTicker.C:\n\t\t\terr := w.sendMetrics(ctx)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithContext(ctx).Error(err)\n\t\t\t}\n\t\t\tw.lastRun = time.Now()\n\t\t}\n\t}\n}\n\nfunc getMetricsInterval(ctx context.Context) time.Duration {\n\tinterval := defaultPushInterval\n\tif os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") != \"\" {\n\t\tnewInterval, err := time.ParseDuration(os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") + \"s\")\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"unable to parse NETBIRD_METRICS_INTERVAL_IN_SECONDS, using default interval %v. Error: %v\", defaultPushInterval, err)\n\t\t} else {\n\t\t\tlog.WithContext(ctx).Infof(\"using NETBIRD_METRICS_INTERVAL_IN_SECONDS %s\", newInterval)\n\t\t\tinterval = newInterval\n\t\t}\n\t}\n\treturn interval\n}\n\nfunc (w *Worker) sendMetrics(ctx context.Context) error {\n\tapiKey, err := getAPIKey(w.ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpayload := w.generatePayload(ctx, apiKey)\n\n\tpayloadString, err := buildMetricsPayload(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thttpClient := http.Client{}\n\n\texportJobReq, cancelCTX, err := createPostRequest(w.ctx, payloadEndpoint+\"/capture/\", payloadString)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create metrics post request %v\", err)\n\t}\n\tdefer cancelCTX()\n\n\tjobResp, err := httpClient.Do(exportJobReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to push metrics %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = jobResp.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing update metrics response body: %v\", err)\n\t\t}\n\t}()\n\n\tif jobResp.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"unable to push anonymous metrics, got statusCode %d\", jobResp.StatusCode)\n\t}\n\n\tlog.WithContext(ctx).Infof(\"sent anonymous metrics, next push will happen in %s. \"+\n\t\t\"You can disable these metrics by running with flag --disable-anonymous-metrics,\"+\n\t\t\" see more information at https://docs.netbird.io/about-netbird/faq#why-and-what-are-the-anonymous-usage-metrics\", getMetricsInterval(ctx))\n\n\treturn nil\n}\n\nfunc (w *Worker) generatePayload(ctx context.Context, apiKey string) pushPayload {\n\tproperties := w.generateProperties(ctx)\n\n\treturn pushPayload{\n\t\tAPIKey: apiKey,\n\t\tDistinctID: w.id,\n\t\tEvent: PayloadEvent,\n\t\tProperties: properties,\n\t\tTimestamp: time.Now(),\n\t}\n}\n\nfunc (w *Worker) generateProperties(ctx context.Context) properties {\n\tvar (\n\t\tuptime float64\n\t\taccounts int\n\t\texpirationEnabled int\n\t\tusers int\n\t\tserviceUsers int\n\t\tpats int\n\t\tpeers int\n\t\tpeersSSHEnabled int\n\t\tsetupKeysUsage int\n\t\tephemeralPeersSKs int\n\t\tephemeralPeersSKUsage int\n\t\tactivePeersLastDay int\n\t\tactiveUserPeersLastDay int\n\t\tosPeers map[string]int\n\t\tactiveUsersLastDay map[string]struct{}\n\t\tuserPeers int\n\t\trules int\n\t\trulesProtocol map[string]int\n\t\trulesDirection map[string]int\n\t\trulesWithSrcPostureChecks int\n\t\tpostureChecks int\n\t\tgroups int\n\t\troutes int\n\t\troutesWithRGGroups int\n\t\tnetworks int\n\t\tnetworkResources int\n\t\tnetworkRouters int\n\t\tnetworkRoutersWithPG int\n\t\tnameservers int\n\t\tuiClient int\n\t\tversion string\n\t\tpeerActiveVersions []string\n\t\tosUIClients map[string]int\n\t\trosenpassEnabled int\n\t\tlocalUsers int\n\t\tidpUsers int\n\t\tembeddedIdpTypes map[string]int\n\t\tservices int\n\t\tservicesEnabled int\n\t\tservicesTargets int\n\t\tservicesStatusActive int\n\t\tservicesStatusPending int\n\t\tservicesStatusError int\n\t\tservicesTargetType map[string]int\n\t\tservicesAuthPassword int\n\t\tservicesAuthPin int\n\t\tservicesAuthOIDC int\n\t)\n\tstart := time.Now()\n\tmetricsProperties := make(properties)\n\tosPeers = make(map[string]int)\n\tosUIClients = make(map[string]int)\n\trulesProtocol = make(map[string]int)\n\trulesDirection = make(map[string]int)\n\tactiveUsersLastDay = make(map[string]struct{})\n\tembeddedIdpTypes = make(map[string]int)\n\tservicesTargetType = make(map[string]int)\n\tuptime = time.Since(w.startupTime).Seconds()\n\tconnections := w.connManager.GetAllConnectedPeers()\n\tversion = nbversion.NetbirdVersion()\n\n\tcustomDomains, customDomainsValidated, _ := w.dataSource.GetCustomDomainsCounts(ctx)\n\n\tfor _, account := range w.dataSource.GetAllAccounts(ctx) {\n\t\taccounts++\n\n\t\tif account.Settings.PeerLoginExpirationEnabled {\n\t\t\texpirationEnabled++\n\t\t}\n\n\t\tgroups += len(account.Groups)\n\t\tnetworks += len(account.Networks)\n\t\tnetworkResources += len(account.NetworkResources)\n\n\t\tnetworkRouters += len(account.NetworkRouters)\n\t\tfor _, router := range account.NetworkRouters {\n\t\t\tif len(router.PeerGroups) > 0 {\n\t\t\t\tnetworkRoutersWithPG++\n\t\t\t}\n\t\t}\n\n\t\troutes += len(account.Routes)\n\t\tfor _, route := range account.Routes {\n\t\t\tif len(route.PeerGroups) > 0 {\n\t\t\t\troutesWithRGGroups++\n\t\t\t}\n\t\t}\n\t\tnameservers += len(account.NameServerGroups)\n\n\t\tfor _, policy := range account.Policies {\n\t\t\tfor _, rule := range policy.Rules {\n\t\t\t\trules++\n\t\t\t\trulesProtocol[string(rule.Protocol)]++\n\t\t\t\tif rule.Bidirectional {\n\t\t\t\t\trulesDirection[\"bidirectional\"]++\n\t\t\t\t} else {\n\t\t\t\t\trulesDirection[\"oneway\"]++\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(policy.SourcePostureChecks) > 0 {\n\t\t\t\trulesWithSrcPostureChecks++\n\t\t\t}\n\t\t}\n\n\t\tpostureChecks += len(account.PostureChecks)\n\n\t\tfor _, user := range account.Users {\n\t\t\tif user.IsServiceUser {\n\t\t\t\tserviceUsers++\n\t\t\t} else {\n\t\t\t\tusers++\n\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpats += len(user.PATs)\n\t\t}\n\n\t\tfor _, key := range account.SetupKeys {\n\t\t\tsetupKeysUsage += key.UsedTimes\n\t\t\tif key.Ephemeral {\n\t\t\t\tephemeralPeersSKs++\n\t\t\t\tephemeralPeersSKUsage += key.UsedTimes\n\t\t\t}\n\t\t}\n\n\t\tfor _, peer := range account.Peers {\n\t\t\tpeers++\n\n\t\t\tif peer.SSHEnabled || peer.Meta.Flags.ServerSSHAllowed {\n\t\t\t\tpeersSSHEnabled++\n\t\t\t}\n\n\t\t\tif peer.Meta.Flags.RosenpassEnabled {\n\t\t\t\trosenpassEnabled++\n\t\t\t}\n\n\t\t\tif peer.UserID != \"\" {\n\t\t\t\tuserPeers++\n\t\t\t}\n\n\t\t\tosKey := strings.ToLower(fmt.Sprintf(\"peer_os_%s\", peer.Meta.GoOS))\n\t\t\tosCount := osPeers[osKey]\n\t\t\tosPeers[osKey] = osCount + 1\n\n\t\t\tif peer.Meta.UIVersion != \"\" {\n\t\t\t\tuiClient++\n\t\t\t\tuiOSKey := strings.ToLower(fmt.Sprintf(\"ui_client_os_%s\", peer.Meta.GoOS))\n\t\t\t\tosUICount := osUIClients[uiOSKey]\n\t\t\t\tosUIClients[uiOSKey] = osUICount + 1\n\t\t\t}\n\n\t\t\t_, connected := connections[peer.ID]\n\t\t\tif connected || peer.Status.LastSeen.After(w.lastRun) {\n\t\t\t\tactivePeersLastDay++\n\t\t\t\tif peer.UserID != \"\" {\n\t\t\t\t\tactiveUserPeersLastDay++\n\t\t\t\t\tactiveUsersLastDay[peer.UserID] = struct{}{}\n\t\t\t\t}\n\t\t\t\tosActiveKey := osKey + \"_active\"\n\t\t\t\tosActiveCount := osPeers[osActiveKey]\n\t\t\t\tosPeers[osActiveKey] = osActiveCount + 1\n\t\t\t\tpeerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)\n\t\t\t}\n\t\t}\n\n\t\tfor _, service := range account.Services {\n\t\t\tservices++\n\t\t\tif service.Enabled {\n\t\t\t\tservicesEnabled++\n\t\t\t}\n\t\t\tservicesTargets += len(service.Targets)\n\n\t\t\tswitch rpservice.Status(service.Meta.Status) {\n\t\t\tcase rpservice.StatusActive:\n\t\t\t\tservicesStatusActive++\n\t\t\tcase rpservice.StatusPending:\n\t\t\t\tservicesStatusPending++\n\t\t\tcase rpservice.StatusError, rpservice.StatusCertificateFailed, rpservice.StatusTunnelNotCreated:\n\t\t\t\tservicesStatusError++\n\t\t\t}\n\n\t\t\tfor _, target := range service.Targets {\n\t\t\t\tservicesTargetType[target.TargetType]++\n\t\t\t}\n\n\t\t\tif service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled {\n\t\t\t\tservicesAuthPassword++\n\t\t\t}\n\t\t\tif service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled {\n\t\t\t\tservicesAuthPin++\n\t\t\t}\n\t\t\tif service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled {\n\t\t\t\tservicesAuthOIDC++\n\t\t\t}\n\t\t}\n\t}\n\n\tminActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)\n\tmetricsProperties[\"uptime\"] = uptime\n\tmetricsProperties[\"accounts\"] = accounts\n\tmetricsProperties[\"users\"] = users\n\tmetricsProperties[\"service_users\"] = serviceUsers\n\tmetricsProperties[\"pats\"] = pats\n\tmetricsProperties[\"peers\"] = peers\n\tmetricsProperties[\"peers_ssh_enabled\"] = peersSSHEnabled\n\tmetricsProperties[\"peers_login_expiration_enabled\"] = expirationEnabled\n\tmetricsProperties[\"setup_keys_usage\"] = setupKeysUsage\n\tmetricsProperties[\"ephemeral_peers_setup_keys\"] = ephemeralPeersSKs\n\tmetricsProperties[\"ephemeral_peers_setup_keys_usage\"] = ephemeralPeersSKUsage\n\tmetricsProperties[\"active_peers_last_day\"] = activePeersLastDay\n\tmetricsProperties[\"active_user_peers_last_day\"] = activeUserPeersLastDay\n\tmetricsProperties[\"active_users_last_day\"] = len(activeUsersLastDay)\n\tmetricsProperties[\"user_peers\"] = userPeers\n\tmetricsProperties[\"rules\"] = rules\n\tmetricsProperties[\"rules_with_src_posture_checks\"] = rulesWithSrcPostureChecks\n\tmetricsProperties[\"posture_checks\"] = postureChecks\n\tmetricsProperties[\"groups\"] = groups\n\tmetricsProperties[\"networks\"] = networks\n\tmetricsProperties[\"network_resources\"] = networkResources\n\tmetricsProperties[\"network_routers\"] = networkRouters\n\tmetricsProperties[\"network_routers_with_groups\"] = networkRoutersWithPG\n\tmetricsProperties[\"routes\"] = routes\n\tmetricsProperties[\"routes_with_routing_groups\"] = routesWithRGGroups\n\tmetricsProperties[\"nameservers\"] = nameservers\n\tmetricsProperties[\"version\"] = version\n\tmetricsProperties[\"min_active_peer_version\"] = minActivePeerVersion\n\tmetricsProperties[\"max_active_peer_version\"] = maxActivePeerVersion\n\tmetricsProperties[\"ui_clients\"] = uiClient\n\tmetricsProperties[\"idp_manager\"] = w.idpManager\n\tmetricsProperties[\"store_engine\"] = w.dataSource.GetStoreEngine()\n\tmetricsProperties[\"rosenpass_enabled\"] = rosenpassEnabled\n\tmetricsProperties[\"local_users_count\"] = localUsers\n\tmetricsProperties[\"idp_users_count\"] = idpUsers\n\tmetricsProperties[\"embedded_idp_count\"] = len(embeddedIdpTypes)\n\n\tmetricsProperties[\"services\"] = services\n\tmetricsProperties[\"services_enabled\"] = servicesEnabled\n\tmetricsProperties[\"services_targets\"] = servicesTargets\n\tmetricsProperties[\"services_status_active\"] = servicesStatusActive\n\tmetricsProperties[\"services_status_pending\"] = servicesStatusPending\n\tmetricsProperties[\"services_status_error\"] = servicesStatusError\n\tmetricsProperties[\"services_auth_password\"] = servicesAuthPassword\n\tmetricsProperties[\"services_auth_pin\"] = servicesAuthPin\n\tmetricsProperties[\"services_auth_oidc\"] = servicesAuthOIDC\n\tmetricsProperties[\"custom_domains\"] = customDomains\n\tmetricsProperties[\"custom_domains_validated\"] = customDomainsValidated\n\n\tfor targetType, count := range servicesTargetType {\n\t\tmetricsProperties[\"services_target_type_\"+targetType] = count\n\t}\n\n\tfor idpType, count := range embeddedIdpTypes {\n\t\tmetricsProperties[\"embedded_idp_users_\"+idpType] = count\n\t}\n\n\tfor protocol, count := range rulesProtocol {\n\t\tmetricsProperties[\"rules_protocol_\"+protocol] = count\n\t}\n\n\tfor direction, count := range rulesDirection {\n\t\tmetricsProperties[\"rules_direction_\"+direction] = count\n\t}\n\n\tfor os, count := range osPeers {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tfor os, count := range osUIClients {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tmetricsProperties[\"metric_generation_time\"] = time.Since(start).Milliseconds()\n\n\treturn metricsProperties\n}\n\nfunc getAPIKey(ctx context.Context) (string, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\tdefer cancel()\n\n\thttpClient := http.Client{}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+\"/GetToken\", nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create request for metrics public api token %v\", err)\n\t}\n\tresponse, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to request metrics public api token %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing metrics token response body: %v\", err)\n\t\t}\n\t}()\n\n\tif response.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"unable to retrieve metrics token, statusCode %d\", response.StatusCode)\n\t}\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't get metrics token response; %v\", err)\n\t}\n\n\tvar tokenResponse getTokenResponse\n\n\terr = json.Unmarshal(body, &tokenResponse)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't parse metrics public api token; %v\", err)\n\t}\n\n\treturn tokenResponse.PublicAPIToken, nil\n}\n\nfunc buildMetricsPayload(payload pushPayload) (string, error) {\n\tstr, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to marshal metrics payload, got err: %v\", err)\n\t}\n\treturn string(str), nil\n}\n\nfunc createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, context.CancelFunc, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\treqURL := endpoint\n\n\tpayload := strings.NewReader(payloadStr)\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", reqURL, payload)\n\tif err != nil {\n\t\tcancel()\n\t\treturn nil, nil, err\n\t}\n\treq.Header.Add(\"content-type\", \"application/json\")\n\n\treturn req, cancel, nil\n}\n\n// extractIdpType extracts the IdP type from a Dex connector ID.\n// Connector IDs are formatted as \" - \" (e.g., \"okta-abc123\", \"zitadel-xyz\").\n// Returns the type prefix, or \"oidc\" if no known prefix is found.\nfunc extractIdpType(connectorID string) string {\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn \"oidc\"\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}\n\nfunc getMinMaxVersion(inputList []string) (string, string) {\n\tversions := make([]*version.Version, 0)\n\n\tfor _, raw := range inputList {\n\t\tif raw != \"\" && nbversion.SemverRegexp.MatchString(raw) {\n\t\t\tv, err := version.NewVersion(raw)\n\t\t\tif err == nil {\n\t\t\t\tversions = append(versions, v)\n\t\t\t}\n\t\t}\n\t}\n\n\ttargetIndex := 1\n\tl := len(versions)\n\n\tswitch l {\n\tcase 0:\n\t\treturn \"\", \"\"\n\tcase targetIndex:\n\t\tv := versions[targetIndex-1].String()\n\t\treturn v, v\n\tdefault:\n\t\tsort.Sort(version.Collection(versions))\n\t\treturn versions[targetIndex-1].String(), versions[l-1].String()\n\t}\n}\n","numLines":567,"startLine":1,"totalLines":567}},"sourceToolAssistantUUID":"498fc62d-db9e-484f-9657-3c95e844bb50"} +{"parentUuid":"0118fc31-4a88-4eae-9e62-87a094865a2f","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01JfEn78kaU8s2onJ4hs7YGw","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"Let me analyze the issue with `extractIdpType`:\n\nThe function is called at line 297:\n```go\nidpType := extractIdpType(idpID)\n```\n\nBut this is only reached when `idpID != \"local\"` (line 293-296). So `\"local\"` connectors are already handled by the `localUsers++` branch and never reach `extractIdpType`. That's correct.\n\nNow let's look at what `extractIdpType` does with the connectors from the decoded data:\n\n1. `\"local\"` - never reaches `extractIdpType` (handled separately) ✓\n\n\n2. For `\"authentik-d67qt8m9kmnc73b2nggg\"` and `\"pocketid-d67qvdm9kmnc73b2ntb0\"`, the function finds the last hyphen and extracts the prefix, which correctly returns `\"authentik\"` and `\"pocketid\"` respectively.\n\nNow I'm realizing the xid format itself doesn't contain hyphens—only the prefix and xid are separated by one, so using `LastIndex(\"-\")` to split on the final hyphen should work reliably. I see the code is checking if the connector ID equals \"local\" and incrementing the appropriate counter, then for non-local connectors it extracts the IDP type and tracks those separately. Looking at the metrics being set, local users are counted in `local_users_count`, non-local IDP users go into `idp_users_count`, and the distinct IDP types are tracked in `embedded_idp_count` — so the user's concern about local users not being counted seems unfounded, they're being handled correctly in the first branch. → This is the count of different IDP TYPES\n\nSo `embedded_idp_count` represents the number of distinct IDP types configured (e.g., if you have authentik and pocketid, it would be 2). Local users are NOT included in `embeddedIdpTypes` map, so they're not counted in `embedded_idp_count`.\n\nBut the property names at line 440-442 create properties like `embedded_idp_users_authentik`, `embedded_idp_users_pocketid` but NOT `embedded_idp_users_local`. Local users are tracked separately via `local_users_count`. \n\nNow I'm tracing through the code flow to understand if there's actually a bug here. When we're using an embedded IdP, we decode the user ID and check if it's \"local\" — if so, we increment `localUsers`, otherwise we increment `idpUsers` and track the specific IDP type. So local users ARE being counted, just not in the `embeddedIdpTypes` map. Let me look more closely at how the IDP type is extracted from the connector ID to see if there's something subtle I'm missing.\n\nExtracting IDP type... I'm tracing through the `extractIdpType` function with different inputs to understand its behavior—empty strings and connectors without dashes return \"oidc\", while those starting with a dash also default to \"oidc\". The function seems designed correctly for its purpose since \"local\" users are filtered out before this function is even called, but I should double-check whether the user's concern about miscounting local users points to a different part of the logic.\n\nLooking at how the metrics are structured, I notice that `embedded_idp_count` tracks distinct non-local IDP types while `local_users_count` is separate, but I'm wondering if \"local\" should actually be included as a type in the embedded IDP count rather than being excluded entirely. with these connector IDs from the decoded data:\n\n- `\"local\"` → handled separately, `localUsers++` ✓\n- `\"authentik-d67qt8m9kmnc73b2nggg\"` → `extractIdpType` → `LastIndex(\"-\")` finds the last `-`...\n\nWait. The xid part `d67qt8m9kmnc73b2nggg` contains NO hyphens. But what if an xid DID contain hyphens? XIDs use base32 encoding and don't contain hyphens. So `LastIndex` should work fine.\n\nOK wait, let me re-examine. The connector ID format from the Dex code would be ` - `. The type can be multi-word like `azure-ad`. With `LastIndex(\"-\")`, for `\"azure-ad-abc123\"`, it would return the position of the last hyphen, which would incorrectly split it as `\"azure-ad\"` and `\"abc123\"` instead of `\"azure\"` and `\"ad-abc123\"`.\n\nActually, that's correct — `\"azure-ad\"` is the IDP type. For `\"authentik-d67qt8m9kmnc73b2nggg\"`, `LastIndex(\"-\")` returns index 9, and `connectorID[:9]` = `\"authentik\"`. That's right.\n\nSo the function handles external connectors correctly, and `\"local\"` is handled separately. But the user mentioned possibly miscounting local users. Looking back at the code, I notice `\"local\"` increments `localUsers++` but never gets added to `embeddedIdpTypes`. Then the embedded count is calculated from the length of that map, which wouldn't include local at all.\n\nThe issue is that if you only have local users, `embeddedIdpTypes` stays empty, so `embedded_idp_count` becomes 0 even though you're using the embedded local IDP. Plus there's no `embedded_idp_users_local` metric — local users are only tracked separately as `local_users_count`. The user's concern makes sense: `\"local\"` should probably be added to `embeddedIdpTypes` so it's counted in both the total embedded count and gets its own metric.\n\nThe real problem is that `extractIdpType(\"local\")` would currently return `\"oidc\"` as a default, which is wrong. I need to fix the function to handle the `\"local\"` case properly by returning it as-is instead of falling back to `\"oidc\"`.\n\nFixing the extractor function...\n\nBut wait—I'm realizing the function was designed only for external connectors, and `\"local\"` is already handled separately before this function gets called. So the question becomes whether unknown connector types without dashes should default to `\"oidc\"` or return themselves as-is.\n\nLooking at the counting logic, local users increment their own counter but don't get added to the embedded IDP types map, which means the metrics aren't consistent. I should update the code to include local users in the embedded IDP types tracking so everything counts uniformly.\n\nUpdating the counting logic... I'm realizing that connectors should follow the ` - ` format, so a missing dash is genuinely rare—but to be safe, I'll return the full connector ID when there's no dash instead of defaulting to \"oidc\", which is more accurate for edge cases. I need to update the extraction logic and adjust the counting to include local users in the embedded IDP types map. I see the issue now—local users are being counted in `local_users_count`, but they're excluded from `embedded_idp_count` and the embedded IDP user metrics. The user likely wants local to be treated as another embedded IDP type, so `embedded_idp_count` should be 3 instead of 2, with local users also tracked separately as `embedded_idp_users_local`.\n\nI also need to fix `extractIdpType` to properly handle \"local\" connectors by returning them as-is instead of defaulting to \"oidc\", then add test coverage for these changes. Finishing the loop and fixing the helper function...\n\nThe `extractIdpType` function will handle all cases by finding the last dash in the connector ID—if there's no dash or it's at the start, it returns the whole ID in lowercase, otherwise it returns just the prefix before the dash. This way \"local\" stays as \"local\", while \"authentik-d67qt8m9kmnc73b2nggg\" becomes \"authentik\", and the counting logic properly categorizes all identity provider types.","signature":"EoB2CkYICxgCKkCZIZ95WaX+WT7z7/Ihcf2J1GnQ5NStWHEhT9LJcRFJ+t0zTDQFAdZK9NLaS4xTQN9GFRg8dwldbXOMvHli0E+IEgxKuYVopYlKlttovdEaDNTQ2rqJXWwn8DjXViIw9LjUTY/Kp05rOcybj15kGgnBibPfmV4RNwoK3BUQjYtT5qtnqKrx+swHv4luaj63Kud0WW2RHo3CTI818+WcKd70GDAzcVchtb77C8ULnMBMNWznJSUcG/4jdcaHOqFe4nV/hDNabbXvqob/rfMsh/slqM1gG6UwDxM1tF1BNKPDYXy4t/+UnOFUxnPriOi9svlNzt266TIiY98h964iV+UaiwKb/rfZAxsldQ7kyTjXBAcftFABilNuIy6jWbbFjm8xs7iMm7yuG2WXdcHq2AaqMqb7YV84dLWysgrGh9MlHTg9/eurDJAne61zDLgmphpEUfgHVknl5BCG9HLJ0Y8LJ6WERCJQ/6qFRLpkcbZ/Ivj9yVPZSbQ0M6UgfToxMB1rJrEdxqGWGKsSK26/XY/W6BdHYSHga+YhuNW+TiqE6wYpUUVP1VwM/ViELvtRjz5W+fzKknXZiJT0cotlXZ5VlPWsMQ08VT9mr63JYh7VXUc5xFMt4GZGwFu4nChYYLQ5dBGOfyPLnuEZS0HFhQxubKkXsCTsOCaxFf/QdpJsHllunI9tPJm3RP0zZndwuqtTBEFg5O83gtF5Sm6vG0XAYXQVQTMpYtxofab/Z7XCPffBUt8VTsGZsQZN7dILQCQmwXNPJCHbWF7ZwlxqiYqv0eM7KTShKOj5KYlZwQEzpL19WAZYB5X0qPCZrPBH3E1+Ahm/PC8yySKNhSp/KV4GjHCjDZAusxlsbbIct4t/qbRl0R+myAlLP88aE0H15jEsw4Gn7Jlwxc0QXgeiuKDpZXb98lbLCIOBKmqAnImKBzxETwUVpYrbhEqbKqfeKBVMi57ul7SY5BJc/V/uPUmZT9+hI1IIbA2VFuvDoEe/qS5W/CiUfjx1citM+/z4yzjKQXxoRLGow/hBDN/txgjkU2PC+8N13lKAovj4xtFW3hB0hxmDnnE3w6sGMEV8EyJeA2kQoZRmM1iDz1TktAt3VbQzofY2+nYqE6RSepTRZhF+zb3iI0o4AqqOjQsOV/v2TcVJy60/I0h0X+6vd6mBfwLIRAhEaNFmUj+uR/H/2wwD+i2QDCdOWBn7AyrWja4nSnJB5zGlrzuHNl2/+7HwxtP504v0HyWXKHvzNP9Z0pQWKNjrOiCo/owwvBIvKiTj1gOBqpcJXmo55Dw9UwIL6rQkOkx6pZrVxvQPJ3kIvxjGhzTyBFrsfh77guDwMRtRKMqi5VPXIBW6YMK5uYIbzbvnMs4W2BtxWmg+la2BPmuJBrm6vnhsYLYRFh0iDLn6Mf8Zn8zENy3ksldYpH30CwVPmVqFh9cYvPbANVAWFwBBS5ePK/MPOsSNFhYI2JRBmxKsW3FJaeZ1hqy6mmkHEmef2KiTQQqiafnQaXpil8qNxVbF1acf8n9f+bN74f5Rt4Y0pRmDelDoJQ7lMkejJjaIOCBiTggxyzX4rrlbJq5j9yEL4y0bIBTa60vTAZpGRxZBFShos7LhZu9oUPGGdY0JBrjGmcyd6WWpbHTKQEeGQ5MrdVB9yDKa3Fj0uS9rPAv5L4ov5NvNCGdh4pODCZgU+kbH/9+Wk3g9QjRQAVe0azlnrhFyfNAx9wloPq4eXCRZrl0zUwLDCkJQz+oKHVOr7bHEi69QtUZ/05923oGyFcvp/+OO+5UXRyt7temjcgzbe0y0AspLeyZUkpKplDTgHjp25atQyrbGZkj1LRST5QQAVuwr7uy6OoYTNcmFHlCwgb2FR7MyWGEYRMXUg+5W/d34xrv5YYfm5k1tmGS9DaGD1G77uSt3lunZhB/YzQ2CXiqPSt3KqIRkLSjdHGn955ZfzQkHDSbWht8lGJ5WeeykEsy/S7o4xZCIt5V5fhPYAXTg3xwkMqc5QZwbYUL7o48oAQD7W2at/t1v41ZeHnZ6RoIXxtBjN5Jsg3to/1OS3dPkVUVj38I78BPFY3I1s3Bcg1FPp1kpunKjylVS9K/H5AQGO0PoYWjrRHrvrIq3VIwARoyt2S+beQp8p7Oh6wPqBUP/lV4XkvjdJCENzdsUDO9TdMkqzsA36Ib/3MRv/vxS1EsxLdL5e3HmljBqKuwKEZe1udGvX+LMMEJbJAyPDXLWBUpa9sYZb84vXmcsdugfhlW3VF1Bm8aYl1lViArWpNx75ZdVS9sTw2Wqv4bXqsTsPairEF8UgBmjihA6P6OpzX2gTRTr8rW8bcN/Y1/rukDBPJSLsR2OVn3PJk7kEWNK6v7I43x2gvZZxN6JavTWyIw6Yp3luXKci/eJkFpkVL1ZFA8nIx0AoIG3K21nEngBlzJ54gXoE7TG70NAznSZPHR/UZYLc36Y00ycYSbnf3JhYsfszQLtA8jGu41PDW2kkWZ0ZpSEDzHs9kDtZTis7bRORTnoOlgufajXc1oCchubnvc3B/K+v55p+UHwz2+UhN3c6k/w50CUXwMq82MQlGgTVKaoP0rC61OgFvk+oGhiAIjGQoPyYalzCH0A3F2FfWwGBc0fkRxNZ+0hbydwfcUbdQoy5MUVkBhQ2KK59kotPojTI5HpFsxrjzvSaL7gosQF+6RPee7ygGhY3NLTi+v+E5qTFEW0sAgzVUcot9NaBx1ld06rUUiAH4QYnZm9r2L62GDIHNp3S2F2wBoyGcPBd/umVv9wePN0XRtm7TC5K3Jm4lpbEAitF2003JBl/kWdnOyEjskwOisy1XMuukuv/fNCpbtSlbdvN9Iskgh7ii46Um9wwtvloIDRV5EizwH+3HPGfjAdvYGZgP3J1/1T0GHSSMnan9xdIQbsJlXiHONqehQgkOUeQjOVnpOygxT4s4XZuBEJ6wBot7pCFFzELkGw4MXl4pOTHAuni3VGcJD1jn7amq28zy+tUzvPHXZzmEOoSZYYiU5vmPa1HEXvgkS+qBjuOLrWvP+d+PnbRHJW6xQcT2PBKTCk/gxNtmVJOIbwIi74ZWpIiAotMeP4o1oKv1g5ES+N7+BbKThNMyCgowGkObq7xvss1V6wR8jbAXx7AwzSlJUjeraZ753G+1MO5KyEKxbclD0BGOyR3GYB5TjqZ+vZWasfJTBDW9h1jLxxE/f6fSB6LXR8yGC2DMA7nRNnu6z9SJP4hNGW8NY9VCdDKvOf1h1f9eaLW5yOIidL++70nZmnRVieW8r6k3sDJULHh7chMt4OfODL9Y6D6OaZGuwoh8m9lH1QWhK9OsfdXP5xif7KkZuDTTR/beKJkHs7kzDeA4cTqUGDYp5YNo8BrMnatQR+/+zoEUDgjs9blQuRtO16jYsxXRn2NMmT2hu+S/lAEJsqma+jBbYMTkbMFVomZuPN20Diazm8mzZqdgNfb2pR5gKL4HslZz3NmLHQrds1rJweLrfdS5IjwT/UyquOBDqNMfe4nOgRAuOgh+NkJYky5DWQJutCqCAzTYTscOuxw1hc61+QsNZyBgz17OyebNToDWVv6nVdZ6oCXoraAt08DjVUbji3nmlelF4pRdkslN4VfgWHW7baqUeMzTDZpYwj1K75MdDiQePKEwWEbztCTATGrpj0Zi755gYJBD5FFxi708M4W2UIzbuXMK203pw/Hbnk7t990YEpfE54QBd8CwetS6tX1E3jt/2RvnG06t8GIVOBFKf2UwQHGiUKD3zvr+0C0rMP8NDgWsd10pMOgqQs9/j/1shKIPlIsD00wpOGjOrnuS6xGUIYo2XfJxauFEf4sh7pmnwgp+l+qVb5Ru0zciK2NUXCXMOKBH4hUz3BP5NjTyUoNAT1MXO0ux1YhRMZFQgKhAUUz1URjA+BScyWlSvpEiGjvZzaWbJ1eqC8ZmOgUGiW2oI6bYFdRjVHhvddWRcbBI5EbhUmw5ERn+ZSY9JiCbl9IqKCEFu+j2/FK/UVRnrRJGP9HP4TXsuz9SzFqCkIbWPxEVwXGFup9nuVqOdTD/gYGUNi6OgYnCs4Dj4IwgKacY1FpogaghH5mZ0BaSL8LW5ycSYrBV/w1gS+wAICYWIE3wbkH/x/m8nQAT0995yfuN9pFna3iGen2A1Dk4vh51Y96xjH1WH96Y/1AUR0xwk2o/KOz4e5JucDQB59Zeait8QiLVH0hG72N8yxES6dmIk5A+I2rozBid9IVjJckp5JF3/B6C5LY70rgB1Ez7s8MJpytBNgSOamogfejKO3WdoaWJ4/h/Q3XRbuQ6dD8pbWetdSIcWCTMxFOUdxmgVL/Q9x8pqwSkj1zHfKxmwW+awTLjzuJrWJ8Sg1tZopuJJ4sGYVxLI6jdTij3imICcEMioQVbMsQHY3omGsXY8AR4B8fuVwlkgaCOm/RM76cqNCyYiizig78pA+xIviSTrmQqM+O5NBHMXmWVJuX3ASB6q4pTJegWJRW9HcKEhZzcFmljlAiLpJUp32vVmqEik7jpaHx0/mW692BAj9miGrwvckzhlDFrBK/fIsSUBiHTgwoSyjCEozq6eIIBb68qbVmEuXov+ljnOWAmDc4Sh7xPEh+7l0pamqdm7vlj90nmfDoqKX3je4VNnEjxy5TM0clQyzpjdOoMtK/Vg3A49qahRJQgPEddu/i90fIdb9DVKREjwy65czeQdHlyO8+mp/rzwGeUHGZSu8B4SOueglNeCiFW43QPoZgHnYvEVCEhyNES5y+RZo0CmfDRi0XXxjTHqMIZRhkyaIIy/YtBpg2x2mR6kFH/0hntOI/ilatjUWFIBWXd7/ywQCD+H65jZaC8gFc70lA+FX7Jlpg5eeTciupocfxJzBtzABqmujDgWDMf19BZ85wBww4epJfuIzLTDMrALfkHXJ7psa2CX/xNw2BlLLw9uwrLfUojkEG54UxtVU7rJSr2pz2fPVHhHJlcjlNRarHxxhYsAWfDBjxL3c68/W3u63iZLiuQp2CnFZTt403tBkc/VNT7wjvQ6tuVtBvv//peqzwYq3BmdbZ4RZ3Jbgow5nsqT1DBcEYDeeperS5/QIOcR6dLrjlCXT9b3EWtSXwsVJnqBT/PkXz+rxG2M7+5g0AcT3oI5bU66Pt2N/ccqrSSDat2Ai1E62wF8Ejk2+itOcD53RWUQK2vSiy0y2ygnZnVkVGrCp4WkZQCz4NapgF/7Bsi2sVa5vRTBq3zHhTmHrJd78HkRQmLY0Xr5GejhZOH37A3x+zbrs1iBHQ1axm1oIF7jeVqPcciJtvtm6qaVRs94cjvm/1jd7FvEAbUmz7B5FSOJUHEH4nbZMOBLeWyMsl3qaSA2aZKOcapihG242CXD93BsOPvKcqiBpzzs528NR6RLE7HPmzRjk+2qLNcc/tGdHNHs/ywX/PSHUnSRDMlcT3EdirGkyk5LRHaalYQeNcARNJzlf6YvBjZjD9cT7IxzPdFiOB+FaV2IfU1liBokwDqdYioIwdbd4KlEZglkEu/XXvHxnleTMVV8jNlLtubQmJX3BrfnBducJFeGn84MlwMrFYTVXc3ANKT8aCNlaNaMwR5UJdkEiRZ1cVAw3W1IaBO2cRdZKIz7XmofJN0kWp3eFo+Fvdt2awWvfcM1KTzM9iypUEuqpQUzPKR+GueSYJEWBiKIvaSUJdVK6p2HxDSRBpqeBhQDRnIZrJor5JyffJIGBJwVK9qyncHnyQbUvz9fEtXqStVer83St1TYTm0VO9ilhWxWlAgYmPpmQDDWQKPj8z68sBjvZj7OvD2VgAEaw9Voofyl+T3ikvnWffwUJUFNa5FwY78Kdr04NirP46KSzCyLsf7QQcfyZW6FNOFJn2v8xTP5fep7VpCDDPHO4Ze0Xo5bX4VNCRak5mtCkONarLtouj6dZh4MQPCJuMYnqzfrgL+aUe/KHOVOeFmvcCX7Uzg+m2M/iqGxQCaB1GZaBam0lyKLArfoh+FT1kQEGLTaxN5WcSj1f7m/5L3ysLoev1OSvF8QLA0a/QX+R/xbZgDrtYriPkI1TAVMtEsapE6hijrJ07R1Ejzz6gUCwg8dL2upVgUKJzodsYgultIDa0pbaBVZ3wmSSlAhku9ffKWFHTO1pRkN0KH58qED0ahLaQjdjkcjcLbrKZm0f0k8vW4IjgfcUKtI+tn19bgVLrTjneMfFYEAXk96UDIeM6yg7BRQYaKL2Uo52nU9tl3/5WQN3kSlXcf3UqRUhhggnRxMlmv1Uq+Qf/yMssDN/bLrm19O3VNqlaW46qLPri2hUYAXEz3iHMesfVgKjYU2YhmQeNV2tfaBdaVAOLoXdQGqc+SHPsKf/BTCaSTelz6BsjQ/TeWA72DmGk3b2CAHNLyHWbcK0JAEQcWVrFVDi8R+jhLxDHlUelvpm3UB1zNxd+SMTbMHsrC0c7SrC30VcYz5sXuaUPXQrQh7q+BqPiiB3FLKhwfzfa95QXKoKs13QZh03O0pDIoPFNv6udiO4afh5oM69sqkazotz3UQuBo6s4ACVwpkebKbzP4X56H7vscGIfOjbl16wsJ+la5xa7z2xFHTU90UwQreT0lluTZakGK2vXu8Q7eItLpGf5pSdtof2X0HTv+vU6BEl4RJVckq1Wrz34YNAhDtrGGJHuCbuJhR73bfn5ZfpzbC8/XFkmOMvF3DPaSArHzg7MVUczAoApubTg9V8urxfeA4jJqxqpUQob9UQDDjKJVhalsfVprKCdB0+ir41Rtz4Rjmg7svhfnySpQafCYuNt6m7F89xEubRWMLLb6sQtRVCf0+b8Pm3154pF1x9ugaA3av9HvzDCCXVxWvljFQUD726JW34v/mbwPw4N1w+AbS0KZvEy33FH1V8Z+sAIcaLtB23FKC+RvGSPHZOU12uiB63N51u1oQ65um018wu+uf32VeUAEM3gQ8/17P3czkOjFhIdo6TjAyFrD9Bvrg+om8ojb1JEZTq6iP4cWXqWkMsj2wqBAR0T+/0XkqiwxudYHfHppJXJxiUoZHtSv53A7XDz5UPCK0VbZu+YQJsoTSLyzvgIWEVD7XoxULH3QiV3N0N9zAh+JUSOZrTQ4iNiFrPfmcjUYdHH5Ie+CqbVsy2SWoLoReglinwFAHzyiLnqSD2MVfPCBcFIpOb6rlt0V03dxws6/5Ci8IrPkblP5ZGjekZ/odcQAWbXJEhqocpu80/P2W7AFlb4GTXZMpmDLJtcgrlNR0aE0MpXmiTwpYc8Z2UfqxiDZqA1+Qxs1nhnTqfV3XOTq5ROZJGDm9Km3xDlAmOYFXaR3oRC30P8LdW+eOIljcg10ZOTWNQ4rbF0nkSlNaC7iRbCK9kg84ppXBQfjx+vnWaM3K8aHU00mXYMEdZ+AQ8qd2TKT5tr+GNfSVDrlgICiXoRYEKdELB2Mqwq187U6I84S2IQYk4cMAgTFrrmPecAJxcJp1ia/Lg/aMmNOENAjN0e85CX5SHNVa6ttAlFz7v1dGFq738RQ4OCqX1yWbwYXShONhJLv6AxD93ogy6mBtOxYV5EM8wHSGRBvZJvnBmCViNwRkoF5vzhwqra4ndUoDjnExSC3+5qZs4Q9SuFI/gi4pTJkk9AHqV/9g7BwloPBuYZyRXQyygQolozjDGuyB6cQBLAAee3LkVyoxVLifBaAB2PJK3UxF2DW7AascRDTTt+EFnWCLHcuPwbrDHpXlNXAN0hrGE9IsMn4lcQubHoxIYAmgYeYpveDfd6+J+H/Os01Ek6ybiDdNxAGGTRlHJVGiYRHzvotmpFwIpi1jK+ckGsbMAmWu14DZ4U8OGZTQEtmsXMyvHqQrqvQLubOUU3vl6drxgswKrCvSPogDn/gPx8kPTM6uibE898DQhmUb120sbS9yv9t5Y/lqkXuCF0MICfcS2mh2Gg4cIyMEYH4NvP7J8hP91M63EEbZjz3Twu7A8l7kIN5JlVidrAaOdQkFxkeSOUnrn8HBlmxgaNFb574Z3bAgQI1ZV3yVTz4C/K5h2ql8lxW3eWDYWs49uaUPaz76GxbLer+2GC4v+4lTSa1nY9gwV5AB3iv9xpT8n/8idHocKyHtaYT+yfcEvIHHscVNj2LVJ5k/wJeUzjKwn6DA75ToSZ+ECGtGvGWHHzV10vMrIJLRqMEQlzHMd4kPAyeevKGfvEKa1DKuRHOAu1w8aXlNVmvTiaIALLd3DqBJHzFn71e7wpSeBTtxvN6Sv6K/OtM4Qd1ab4CjRCVNSZysqGkKMmChihx6X8QcfpvKO6s4lCJxQnoaPjeqVscMghlWzvrqW4TS9+AVXw2agvRxQukz1Vr0jwIgsG2SLdp81fnqVDVwY6RZ3OWPbP5+C1raK8xRap0iKvIKuoxGdWlUOSyarH+3Z/HrExEgBRHXyHFo/kNrtGvX9HE/x6bFEu7XiyFxrQ7xITlMReCr/y7T4Pc9hrKpCiE9GiocbEwF+o8cQMSl8tSJZsWJgcXh7fqppioh600KtvvYzQhuumtsuFv1zgHgSX0urmUQqsiDrr0+WHMvInVkKdp0m1ZJU/XFlt8TeoT4RocLTZedF4cana/fF8CNxPjFLzF+fGU1tQczNrVMW1qQdLHGCwaa4OxeAqICbZtdYO2cojEohLau6V+fWwodyPui0Q+B+gcGO/ADFGTHvNMfxyl1YsqjVjWKVqQ8Sz3O8tpUfoXsQUVoqapN97gi2rM7RnKyR9KWjpzbl6RNI9ZBrRNqIZZT2sqdk/CmmUvHA0JyeCnGO7dU/yrKzPnWA1CacF/c2OwUme9euKmDLTrJdCRndSTFWgtXDnYhXfqv7SlqHBLomASze4XNiId0Ww8O/PQ4vWRIF+juzp7N+cYWVAYyGDhnOazPv8lExkk2ZyJAXoKR/LzCF7uo1MnPImM/mjNgIrKg9duVFVJ/j9KJlGDxVd9PgyVzNzB20nscdyeqTX1kRIo0lzr8RBkDKdsmCXNKY2+Tqa+BD7bKr1dZfUpMuTmC9eqwSlK3MusgQnmQSje4hyCEYBAFZbRokYDiFWY33rFrLkgI6qtYq6ZwEoQ4sEgXQVZsOk9iDDfO8ZUdtTNQ0EE2ZLJ3jTpNA9/vx4DIVtH9jT9OZ40Z5eGYcdfARFs5ODRv62PfMln6wXDQmZU7OTltU8N5NaRBFjWprU+X52LR9ey8adfJ4DdH9PaD+PRV0MrK5htUfMf4Q4iWdYmEzXTW1n3WbHNkTPNfPzmcEqTWP2SMrb22ThovH6zpnehdRm7ZsoLM6bQuX7ou/7qipreiI9RNaQE8u/FnYSFJC/uVghX8cjA8c+Au9lkbTWa+RB+z4KVXJLxihym/OAsLIr2zRtHwaC6PRlZawYnOPLGSaeObtR8Ml4fzUwCscAfh36HS6oly9pL6uZ7HYXD2DdLkApxPFfgAW7lKSfzebNmZFVtS4V/Q+Y5aTIv8NGJ63Av105wwM7HHN5Cal5GTgDcqfwXdlxW3YoKBTHL376+c/P2HcqL3Fs5A/y9HvCsCiXuQPRCL+Kg1ptTbGSF13nLIF4IkjP+9tjO+OCe3+rm12DbqjHLphrulA/c8vHZrKU2IQDCHB2G8H2JiOGhy++uTlTdu4Eu72FboEgsoqnvYYOPw/viwtGMl1xbWQXMhdQL7INY+EkumdfUOjjem2AYz4O6t1AsxsXZVlDgjB5WoJmbZWK1SiqW2BsaQO4J5JjvvHPJV/tt1b/0fwFWC0uis1Ij/YsKceNBxGeqA4/HGUAEf/DWXBEQ5KNNWXaVhENb1uPW6JGJYG+7wd2TCLtkAyhq5JrpgmCI1EuSC8hHuJ3+nhW/p5sVmwvJvpAp2zEak5Y2Hza7Pr6pJ3UJ2svUY9Y82/qhqcvZjC/RYF8rjN/VFC6uqCAqqANixUA5X3bsjSTMzCYpnARzVropzPuMEEMMO8IXPf/5W69nb+ZtnaGOUVU8satyHv64I+PL9KZwiS3waIBw3sTz/zX5JxorloijWSbPX8VWuUrmrVdeM98M0gEPZmBwQ3KtwdnyRd2l1IpI+7yy1+CQl67qgLaONr1tkIWN/qtaQ5vQgCnrECCtZFTCf2L6njoxliMOahn6GHidthOYojAfXS0va9ERBp31v+DpuVXlZbXQQEWzaNXn9EeLIjC6DZ629qsnVe/RBVCGBtsR/ELbHdTvBlW8YPZLRravZ5HAI1Bmf5qGn6ytJlmJEyvH21PTrtjetA3IMcomYOVuz+xkPenTcuhmXB3S2FgS5dd5cSQynHIdExIqU8YEzY8K6WpkCFBhKxDwpdrSp4ijqvgW1Wczz2ijcmkZrZHJEkk/FOaiRh9UGvaAR2i1POgDpRfA94dK4ceqwMF0Sd4Fon7uHgEF45IYOYw35YrfqZ6pe6IzNE7wwGIWkqoc0zWQV9eRFEjT+q4cUABdV5BbxBMOPsjqW9j+key+5Jl3HSO2nvQm3BIA1z1q7inaa6wRPRPyGr+YilP/HXtkCx/Ro/3WeJeI3NPktNX5UHCO+eX6EixcE39Y0yFoul+QdMGORTI9Zcukd1XBVkibJ3IdlMayXynYMyHcMKcSoaPfsKfM5RNrzCMND5o1NtDIvnYsdxrrRFH1si8I9tGyewwXSZWkCfl8Z0Rbpe7U3BMnY5WJPAQfGVthOuA0a6axuaCPYD1JaRh/OlKtmVjqbmbVMdJaDIe3IbPwVQUYbceexzS3U4TKzTyBU6fJLZ0rurMh2SmdhM0uMSrwG1/nEY1Vu4EuFqFrIIG2p9fnShwHj74s5+12XRIoN3+uCxQIBJmTBdybeZO0/C36ZWunbaR5QehlBMhK5eDIpNbMS2+BVfmEmmXfjtS63wJonf3EFcM9OWRrGj3G8ciYVuKNwaOizHUhmGZgFAZ+aYCV9MeRAfYRURbvOXKlX+/oIqd/LzJhsrHNHP+LX5rOjcanTE3mm1GK5Emhs3z74YIlVR3A3uzZLnyt1S3WDLoFANFDlXP7LkXzbR47lWh1wLtagblTq/aQXjjvMMheMjPrDTD7q+KLTqbvf5PuxeA/UvQ0w2Eiufajk3r5Cte/t7PECLplrXpXv5ptOe3Z4qDBjJPnpYat+ZHpq6ofWequLGmx3T59X+FytGMK/GSsBugRXWudTQry8DYvg+lNyqpHrU6OLdNWoztHdLgmpNTFYGVHW+BW8hxll0ZNxRR3cAZQC3gmUcq8tv3hmk4Pl02ghakxN3GPVD04Kw7RbhciZndHNzIsEvSONMgBDQM7Obv9pEBMUaQYeCwZ4eldpQGGggqXY1XpFI2JS0I6SUxaMCzEXhhMkmPDtutyofvnZFhy289SpdOWf4Er8N7O45JsKytC+3BZRvv89ecGfQKajj7qg6tguqkQJwrv/BHp1aGMDcnHKCYEVGEzTa3jRlF+Ixx+aQIHcbPF8tBbSs9i5j19L7YyLmrYMzEjoWiKYuFRAC/AGqCi8AviCy263zgfPxqSb2blJAEhhddfTskLnAVXDHOQU3/PNPXgV/gcM7lwBh2nk14IJRYwH3A4l3eWpU/Q4T8zl6vpcu7dVpoOKkIEoNVn/rBrIyIU5BZjRHYOE7y88zzDPnAXzQSq7jTZEF1G/KFrYBQnsFPwiIhQ4ZXZP6yiMV9SamjafvSEu9+lPnfTaTgioRv2rdiXVkt4TYSluRy5hjALSGlHcRUI288mWhXYNdQHO+NJPGNkG9wBShSkyWJ1ZJAMgzhfcte47p0GQoM/dFdbSshjE2OgaEl8UbnJWP60kr6EGQkEBDcSY1m/XvuFTC+T5urgrjAy4dUf2Z0sHcnzjru37LSLq+cYfVQ4CpdnDBCfdAML93z1ZCiZEFVymZQdo/3Y4PRdzoT9BGfXwKkHe4FblxogMC6Q7Jpb4JeQIFTOGTnxKPrl25HqI8ee3zv96v/3suwpMaHQv8SiSgS6vAdJnXv+yjD/YMsrhz4XLXJpIUrVqeh/85Ox73KPZIoV2AwHdUt9bmpMkOONBwNnV9e+Yw7uOT4m0aWuIkE/qdLRcQeY8UJVvmNAcs93uq/YfLmFGidAQiZoYmdTRDXVw9SuIEoCTHOX4pDc9ACJs4PBWBnOVsyBdrCyyVTvrXCBCzhdFZES0+Mb7wBbrLXCVzAlWsbXaz0mmnFm9MYFcGlGkRclwywHlOLKLI05dWvRYofAmOt4Nfh/dSkQGGQ0n4CsvU18MnRO66oBWC07o+y4rA2PLFborHnU1Wl9FeVF1iNZduAkFD0QahJ9OPZiLHfNSl+d3Q4DSuJKx0C6Lsz/SeVmwa52I5dLUqkhwLUGeVKXvc6+htSPsUz3LlsVxqNa7MoXu1S7BL/Pj/Mj0B8ZzsmwMDqP+wje9KjTCPbpopMAx3gdWBYNJzfVM7QPQSIsORl3cwxktE6sqkRlk6w6wMI2qfYqJD9f4FmlAGYH19iqTH0Xv6DWMFitT8tbGQW7/rEG4qzZ8Bu0X2ZbfHdOtbrivv/nIO6yS5Ci6Eqs5Q7Wzs2hQBFyaxdRoS/KIK1Gw3HD3l6Ta+PISNUyXxNLpWf9nvlLd9AX+MzaHZAYkuxlDfmKpIv6TQQWvnzRtjRCKMbLIeY6IWE5Baxnoi8NYvWQE8w1d0uZMRYp4LHqojEUTDG1s31eqfuRFGPx+8xYB8MtV17fjX8sM8DngwiFhV1n4jw+HRWdvaktGutyDSeIrqJSUaHKOAe076DKt9suLgEh2M/iFJx3WVuDW26hmR6ntGXDJzTa9DbO+bv7pmwVD+OsfguqLIc+Cp15ydGuauM4ETTk3oILFw7uZ6RGnTvul9qFQxLjrkrIeKcTCRT0BSHTJOI+GDN+cAdpiQj3Woc4XPJKKR8sxReYRPy+c0T6V3eIDEZVxkbIGNkRA1snpHAl+Cr7mZcsGfg7cXw0L1TmN6RDuitffBG9OkHVRHA/GxvkyFmTaXsI6IdzussxcRlR88lTvWtjO4g/vpgNfSkHzNaeifhTzEJCXm1dwCVyZz4qSCdSvm6Q7/QNPaSXJWi69tuoItdX5QRiFjvp7fT6GgRb3cvRjdUqPs82a5PdG0EwiqorU95afK3vUf0xnZGPrg98kUsr/6VMtdb9RNzhJSyf2ZiMoVrnurMypw4OBL1ewt68HjToDfSLjUp9DBN8N7oStsYjNEsIuW9KJvkKdzQYwKSpeuR2kmg83T2Te5Q55bgfLSoWUXfhZwdxG0K9+xpv15C2CTfVqEsV7G3jMe3f4VzEEvgkyGs5sKTkLaytVmX2CjyF4XGsK57w9db3vA2kgAeA0xxPqVr9AhCmr0VDzE500eIHUzQ08caBqs3kMKFZMon6yrLpDt0kfkJeZEl/eEp4MF/nHFsR222RPMJ68HXFV3M+lYR16ggYjJ4X85j/vjWQrQ/Q+d5DQcJ5Sb9KknDwXL0lsxv5AgrQ13tPvRE6x93uPhWP3hteKOIOcz3kYyAXWa5GTBaM73Df1smtgp/fBsHcSpAsnK856eMPbdjkUTAMs0on2j1naHXw1IdGI6+IunRJoEHK3k7TM3i2jxV7hzCaS3WmU6oGSnZojcYgzgGzLzkQDhcyenwjmJITJpUEShiqsnbWgLpwG2Marim4f+0Gimzw+RIudPXLdkiicgQZDK2f0iCk3Mn+/updiiQVCMtAxZpjgpbwG3iwJuXfZXRBhHo64/mzGx8xmeeHiDXPivkTcKIoeyN9AOnlwi7xYKinwSZwvOB49ugvFzCLzVrjybgp0dn3bPGXWRCLedJodWnuqv5eM2zYxZsdnpXQUuk5xdtqxFX1EblIYYfZV20Dyfdczqs6+Dum+6XlwQrcsfRKd8IENQe01+mPsB+mmudraXUk9+zmLrVH6vGpWuBzFjr2DlXqvZyJQIEEc7PRxxlhJ2d3FFMcDyR2oSZJrqewuGBMj87JsR+sGH8yiU1U8DFccJhSgTgBLL738C5L9mIflRRuzb9pDotDT06sO06SeZxYEOQ1GZeC0Q0DvCtxe1QH76QGdGPK/fVsm173PhXfaq3qyUt7pSgi0OPY1rtMiiKbPl6gDzAJ2SZw80Z3Oau1o2r4xj2tDE3Ov7hKHJE0sT4YVpyKAF0Op8g+3Hn5CrcHnZompttTzZOGfptlIdfjYJ0ED5dcysfu3ei4lWLjZeD0zxC4tT9BnP9PJAoEfx6hnL71YzAUEFmCxWnce6N5Q8HACswBJF42m970y1YxEqFJKqmg54+xjvtbN3Vvie+zAZw8sdLMp+sTfiUCqyH6Cg504j7GiuQHVaYU3hfu0vhP1pthHAKBr00tvmlZ8IwnpmfbikiwMK3yXPhVKNWC7sIDnvycWyMgkmlrurbSs4mPvsjYGBJMeD8khPZ4fKaGIKFH1+/uI/KLI7e2Nre5XVNOTHKLP7lQ7WJfDMtcK1+cfdq1lxtR4vQEmo/SDuzhUrGifkLzueilANdXZHmg7aEIv0c77wgADznHi/rwvM33no3H7UU2d1ekx2w3sbBCysKFkEY9Z/ZKVOTbNledtwe7gTwy+7w56XwFVTKhIAU1f+9UeEQIGQbr7zkN0yRECkdMy5Fu2V3MQdStbcEUc4t3m37LMcLZPL4YmSIo30eRYyiCwCeEYcRIOv9hRi1n0Crv2ujU0wU7z4RbDHwGCIb2qu82gsmMC2Bym+/iNJs3cD9EONe1LTppuQAAWfLVRrs1vP8+Mzq8B7JYOc+HcMFpXyDDQG6UwIOUn+z9GOHf1Flg3YHgXJs7/LxSpqXw8SoRp53cJQjv6Yy+5d/PKBlayulBHYdVfLX0EkNouh9vf0n6MUZ/TeYlQlKdmOb45JQQuQocBo/Dg1U7ZMA+4FqLOHWyRoBQ4e17Wk9lwh8Yxo5rXFcCwoHp30GloeHk/6ESxEqAdg/4J43F2bhEQOUhuRfJfYZ73bQUtZpAq6BkmMLyvLm3s8r+ONk3hf/uRfvLM+tW8AEi6kCbLn8riROzQS82FwYp+Z61PFFQSOKrJY8qQY5iBfl5N4OdQY0GZkBZK5Xcq6KgPZZR6iHmAdcx44YDLu9iElP9q+TLlte1vVnBhhfg3an/mPFRm8kGCePY4NF6UlfVXSiQ218jY8PmVNhEOUvx7GviAbQlynbQwx0y1Ua4q1Nsq3QKsANwR3Hh+ooZg/MGdl4EYukZw/rFSDKJOVGdathklG+N1Pmo4fNr6Zc420DSdsiugJQmdSgN7NrrtAnTOObpyWaY/vG8RmTyr30+RpmDIwG3bEgvvbMeGdaVXKTD0rnPHxWUAT+94By1P62r2C6zcKc8H5B7tk6qF+MLTx1LOisGJ8FCvf5UOtonmCfZkHFBZqHVoB9XvySeYNaEAHLbIxQQQKvD1KBKz6MKu263JffVPOTqQAn8AwBVrSOMYYAGii00fyedVvlAjlEaLk7vM2/FOPg/PspcIXX98vZ3BSkisVT8M9Eg75jpWv+CJrfgQgsJwI6sG2O/duorRnsvIhkbbGrb8szogzR/cTXPOR3H84fZttIc08f5aB76nvdfWsb3bcocAkUC5AUuW6FCEGnqANFyfT7nClVuoNafMMIIQR2cEkkhbgVCZvALp/NMtx2BjIgGxIb05X/tSynRF8uJiYfcen/XwPdDal9qTVArfUb1+ykAtizNcxIDJoQjlkRj4WMJOQFKf7HsJG7sqJLcTm7R8KeHlEynwtguUcRjE09Qk4Vr57K03hL8UZUByos3AyEaxE9jUpfOZBHzdl4Ll9fvP65pqueP/nurMLlFkKjkhN81gb4gk7nU00R1gTjpl6uKRrqxFnp53d8vFbJH1PC40zQfe2HC2k+ozEzG5A5lm/Dj8Yu81Wf1laA48RgcpUt4jimHG1r9Dncjx0tlFawr4vxxqt7SAjc2NffFGvqXLVy0qNpiHm8s60OELbn4kkxnGtta2BKhrOO4MyL8ERtrutHPeBkXI/iuSkT3/JDJ+oqe9seyUDy5dOVBtsBpxW8Oh0KPqkl4DXVQPQOgEC/l//kmAuizYhM5kNRpAGnLTnUXTcre64T2y1CfN5Z5yeySzLtmyw/N0VO7hM1M79NJGTI5CzqFupYRWojlMqKwZxmJYD0Gwyau2NCttQBkiVKotzJJVY2+jaZ4ppsx+L7kE04dDmryj2s7y1cWYWPUDL8qEV59ILS81uVTcll3cfX0x6+zZy1Cj9XWmiop9BLNzht2kd+YNg6lZIFfj9Xd29Ne4ObKm6EiUrlxeU2CGAselX7zIv50Q8CKrD2LWulPWcZLJW50UiyWskJwo+ch1QLVIISF8fFteoQmz8aACCOLoJMcXIhJnPYDhMSiszXXQ8X1YoDC3Vrk3WoWS8eAqsoey/tUTE1PlIsSmet1Tkc33h8olWCovDjXFFnsdhdbWhxZY285lkd945xVDbfij3HafbwqoVurwCi7yYamIx6rR4EocL+qKbRsNV9XzcTzXP7Dxuj2uQFQhjFwl7/ba2r4w7R5IfeGRXIMikI12q9u+0OK/ALyUbkQEU9KiBPJ/vN1XfZsPYDCVpGWPiyaNMZ9TW0GUOHzo9tjVfIPDjCj+K9ffm88mMKr/rE+1w5ttosCvQqY5wkgSdUY5sVvC9w+kC1IciLca6F1KDeTszVmnG4OwjBL2sNQrYXeUf/CX9amT7z3pI1RlpwZbpI5tIWj7tSzdEUJWLLsJjaNK/FOcIefOIES3yx0+SsaA/7dBfSt++ww6xaOD4CAj+GB8j6rN0jl57j4ule9HpQ4owxNeFzDOZUCAklEFwHad9yltZSXcwMC7S3jUQdvsr0BfTjSxOWfVAnbW9QIk8MZRLfwy5HU9oa1i+7qNqJy31khRuG6ST83MFtV/bWBOnDyZj1n9dk4+lZzetLG2irNpMVweWePqWB2zPMaGWsP6bkp4qU26SVWJiu5TvedKOsZYrxQy6wOK5Y4rkjHVqiQj5HTZp1yLG/rd4bYP75XxH0Ni+4dcS2NpBZJEbqcXO8rGz6LRFJI6RWC5N+6rMPoOAB7Md52kenPXxpmum3MydbIJzjRfmHHi8l6AoSu64J65RYkjxVbwsQ17g4DMj/07O2yLkdndLvMj2/UOZmy7CVtF763c/RlyDnbSdTtUUaoUwt6QrvUA+TpAGRhddUcb/0Sy2b3t2CqCDd24QL6hq0ZMMfxzVkpd+gggnvMepQZ61iqwrP3QAWs4SI9fiqCJYn49zQeBxrhieAdsnb4IOMec7KwI5Cr/z4SAz+CVbb/BR1QEyJEkMFniCoNpgyhiVgbU+5e2CxmjhkDxVRFlrbXS19AEIeSY6kGq1L8CmTjB0ArR8OKYNjb+jAQtWPaZkKEBZQc7GN2hlcUmch/CrSRGSAoq0f/Q/KJs/Su7Fr4zrB4YHwAQZCHsDWRq9d/IvinVMBn6OdfSHjQxtq2FO9PZyTI/D7/BdxDJqxF/mW8hcqlKejRxJSO5GGb+b2oMH1UZ9dGZeoAYNmr9dzd9haz1VbGTiVDfZX8w/h89PeP7e4c8RO6JDD2PGODRxxTylXQ9h0932Pl+KKrteQ4PfLYmaboyzUPiZEDaXFbTbo4tKEgMLy0vnHLIHyusfqSyIllFBZV08FfDNjm7+nwtAdAxi16ALyxtdtT4GNkT4WHYlqwkEP8M9y0+BOSdcaWG20Hb2x85KlDzHuehaq08x33yCxxz1vW902DKDAjpCUpYt1wqTPS4LenONzid+qpxSNRHl9z3olc4g44F4nNcCWaxgGVAP3EKFRIjCQ1UoDNI7yyOi++HlGxzDVVEUL/bGS+pmM/9iFo/j4LOCAcNzZA4OO5AwNifZfJtJl4LT+Pqdvlb5prVxQNa5qFt2wFIBnbGcguNr8TFr+MTAjEQ4JV/eaSBVlRitmZTcFr5t+HR7SxokkFrlAFb5Q1er8GRgfvSqVKQy4Uzqx1Ru16blmPbrxEb5qLOAwohgbEaM2+aqv/CVHH4ENF2S5NcXB7SVqUh3B2YxwWMbA28Uk9aTUjM+93nMAwkcLEl2a49Nd/7/G/+gQIeFaehTmhtY8iCFuzHh/n6AAKtBQlpOsW7ag+/GutVHKOW5StvMS3GECThRGQbUoL2tCdxRIQaOs/Yb5RH9Bfyl9g6Bzf5ndmAaOLDKbpH8IfTRFGeOBxliJRfSJf3iHtBNgM2knGjJ7TDj90FozpWcGRNMYqLiYyADO0NCebbsjH8NiH7QUkze+ToURLECbRKJskat/iZWNypeS+iWs0xc4ArI524qRiwgIYd/Yoc+2U50cUQTktNyxkYbLuTqLW2pvrKxyRmecViEPhFgML0gkjVTCibN09YuYUrChGxRNKV7BUm4J03+/Th90e7lJW2aKLfKY0VTmltzlzoHmBAqTg8k5nTqN24sQ/wy25owmHXgnMURfZGrjHultIRIdTeFP2TcKqhtCSYtwYK5KWWkKyr6FBaJOnP78vinNtSp2TjxTA24V4HxBK56hxEalSSjd+dyh27JlR4rWscxCa0OoxAQDeImU1vpu+12hVDNp4PGwIUH7o5jE1t8G4lFtECdD6pCBocAwoocOP93GllDSo1RQ0+atdJO17bFt1D8q4/1wR8Dtlhs5lBUPVopdpvE2APPTBFdYhIW58vxUN61GdtIWAP03dUv6IhelupSXXyaprT+XdyclgdEUVRCjPR+h4pIoG/4s5Ki4GMaa2EpMW9NoungMc11TNhsB1s3zS4o9s9mdBwqbtIggVESH+6StA17mJrCa1eYEMNlsdwJV7wCbwx+RkbfjRP9Eeim4Ax8a6dF7SqE3xrKDLaDO3P0cYI3SUC5FGjF0pHKbb5u+/LsdKL8Ik/aW/u/FtUsfjDsYEgBLx3/pTyFo4yMbhT9vRws5xKnIXlJbSsdRFHz+VF5uk+pkaYvpBw3j2JKvo4Nt2H5JZIkEqI9Jpdotf14+Z/TJdVlvT8pWO0grKW4QlSlRJkQHJ8ykhx6bBbM6tAbcg3N/dBi/EypVkBhex+20U5zK1a/Bps8SNOj4U7FTN6dVg29nieYhXDbt14m9DA9IoO1fqbxryTQpbevR7jCYszQi9sOr+H/J+69XJNGzOaplRIu50P+Prqmmq96TAx9Yv7eGgQqyOa8xvFepBuHnBfNi7aX9pCjlYvakkwZIyCCCRZ5QqHPrvebV5MqKJDH/ID6ORZ0TbWRx8wxDnlfZsa3x0ikBWu2INO0DZiYgvfgXsw1UxbiFZqq2ivRn3haC2YypGTPoAl52kz7bh3UvjCdgOPvzHl857tTCbk3aYdOfDWTKLYfY3+oyQoQ+MVTxR6kiv4fovzUij5gG/+Ooqidy/wDB54/LRrHF9kJwAQnBHTZqLBJCWlSvOHleO5y06XOyj5hl5FUCvj90UjhLJRbC0EUkYCsyJ8rVSDaZV4hnD9mmYXaYF79DMnWzNjlhV+09dEpbMg0Zg86RCI+N1xK2xSPGNXJHWVJTvwqUjCvJTcYaLAu7l+DDKg2YHWODlTyoItxhszmj19/SNF9TxL2fcuaq6cg6sFbcD2h5ocLdlw/EyW0jmP6FZZZpnJG1PKs0wwrsrPpe08G9sdeMNrmZpCYeUREMCMx+Z1iBUbgyMSohTfCa7IoiPhTU+NqjSQ9YC+2hKRslPv1pNM1qzu6swR9F2Rv73O7m750kIdStTLHeaTTfRdRmwWhTE7umuVatDHaVWurL2hQfkvxLUR5/ZmBIP2DWRE1Yo6y/S93YdRiLbK223JmvResTxc+qXrn608/NEnyVyfeQjHG2ka48wgpj8W2eLdvNLNsHYl4tia06UoQQMJuUvjWbhTydRuVWIJkq3rKsdNDKKN6NEROv2oatBzhz8fGtaw0jLFdnmFg7QVUzaf5zUQ75iuVpBpg1fILS9RJKaOU2S+Q0q3SXBU9JHc+UY3mG4Q4ztkTT4noNx4tzaGzzltyJvpg0skbN8xnrUOKJXdkZgmkf9OLhkGGtOU/X7BY1M2VG8VUcQtTwamtPoIOPFAH1xXEjhjwm7BpsIsOSI5YYG3sjQTa5vdboL7b5isIzSlkIX2Yd99QJ1ZWhWcygM8YR2vIAc6533+2SIzk2u0jv5YO2gMSqfkPbwcN4b47kXSftHf3b2ucAj++6m8BCKlbs5hgExg/lmkmHvk71dSln+tPrgyG840ISuhWjs+q5Fi55tr25j6Wk569CsztGVHjqqRDP57dU26irIkYAQ=="}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":7379,"cache_read_input_tokens":96769,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":7379},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhpqbxyvtfd15ZNeP65b","type":"assistant","uuid":"cb5c3153-da20-4061-a802-28b3ae2056e9","timestamp":"2026-03-04T09:46:34.883Z"} +{"parentUuid":"cb5c3153-da20-4061-a802-28b3ae2056e9","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01JfEn78kaU8s2onJ4hs7YGw","type":"message","role":"assistant","content":[{"type":"text","text":"Let me trace the issue. Looking at how `extractIdpType` is used:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":7379,"cache_read_input_tokens":96769,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":7379},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhpqbxyvtfd15ZNeP65b","type":"assistant","uuid":"e077a30f-864a-4a52-8689-310211c145ef","timestamp":"2026-03-04T09:46:34.971Z"} +{"parentUuid":"e077a30f-864a-4a52-8689-310211c145ef","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01JfEn78kaU8s2onJ4hs7YGw","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01BG63MrJKEhoiS6kkgRTFmc","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go","offset":285,"limit":20},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":7379,"cache_read_input_tokens":96769,"output_tokens":5038,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":7379,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhpqbxyvtfd15ZNeP65b","type":"assistant","uuid":"acfc8fbd-5bc8-41cb-be9d-be58e7105628","timestamp":"2026-03-04T09:46:36.036Z"} +{"parentUuid":"acfc8fbd-5bc8-41cb-be9d-be58e7105628","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01BG63MrJKEhoiS6kkgRTFmc","toolUseID":"toolu_01BG63MrJKEhoiS6kkgRTFmc","timestamp":"2026-03-04T09:46:36.153Z","uuid":"4a3d0446-eeed-4138-b571-bd4dc76a6792"} +{"parentUuid":"acfc8fbd-5bc8-41cb-be9d-be58e7105628","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01BG63MrJKEhoiS6kkgRTFmc","type":"tool_result","content":" 285→\t\tfor _, user := range account.Users {\n 286→\t\t\tif user.IsServiceUser {\n 287→\t\t\t\tserviceUsers++\n 288→\t\t\t} else {\n 289→\t\t\t\tusers++\n 290→\t\t\t\tif w.idpManager == EmbeddedType {\n 291→\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n 292→\t\t\t\t\tif err == nil {\n 293→\t\t\t\t\t\tif idpID == \"local\" {\n 294→\t\t\t\t\t\t\tlocalUsers++\n 295→\t\t\t\t\t\t} else {\n 296→\t\t\t\t\t\t\tidpUsers++\n 297→\t\t\t\t\t\t\tidpType := extractIdpType(idpID)\n 298→\t\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n 299→\t\t\t\t\t\t}\n 300→\t\t\t\t\t}\n 301→\t\t\t\t}\n 302→\t\t\t}\n 303→\t\t\tpats += len(user.PATs)\n 304→\t\t}"}]},"uuid":"6ca95002-c343-4d48-8945-fae93e96006c","timestamp":"2026-03-04T09:46:36.152Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go","content":"\t\tfor _, user := range account.Users {\n\t\t\tif user.IsServiceUser {\n\t\t\t\tserviceUsers++\n\t\t\t} else {\n\t\t\t\tusers++\n\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpats += len(user.PATs)\n\t\t}","numLines":20,"startLine":285,"totalLines":567}},"sourceToolAssistantUUID":"acfc8fbd-5bc8-41cb-be9d-be58e7105628"} +{"parentUuid":"6ca95002-c343-4d48-8945-fae93e96006c","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_015QfasmpAWA9PoTiuSykgU1","type":"message","role":"assistant","content":[{"type":"text","text":"Two issues:\n\n1. **`\"local\"` users are excluded from `embeddedIdpTypes`** — so `embedded_idp_count` doesn't include local as a type, and no `embedded_idp_users_local` metric is emitted.\n\n2. **`extractIdpType(\"local\")` would return `\"oidc\"`** which is wrong — though it's currently never called with `\"local\"` due to the if-check.\n\nFix both: make `extractIdpType` return the connectorID as-is when there's no dash (instead of `\"oidc\"`), and include local users in `embeddedIdpTypes`."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":5188,"cache_read_input_tokens":104148,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":5188},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhpwSKicHq7E23r3JMXE","type":"assistant","uuid":"05d4bcc1-1e8b-43a5-8a6b-7991164d4742","timestamp":"2026-03-04T09:46:43.135Z"} +{"parentUuid":"05d4bcc1-1e8b-43a5-8a6b-7991164d4742","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_015QfasmpAWA9PoTiuSykgU1","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01F6wPZcBHfjV2EfbBefiUgA","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go","old_string":"\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\tif err == nil {\n\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t} else {\n\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}","new_string":"\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\tif err == nil {\n\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t} else {\n\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t}\n\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t}\n\t\t\t}"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":5188,"cache_read_input_tokens":104148,"output_tokens":487,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":5188,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhpwSKicHq7E23r3JMXE","type":"assistant","uuid":"c8587c24-db09-4ebe-a6ec-a60f6c712d32","timestamp":"2026-03-04T09:46:45.488Z"} +{"parentUuid":"c8587c24-db09-4ebe-a6ec-a60f6c712d32","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"type":"tool_result","content":" String to replace not found in file.\nString: \t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\tif err == nil {\n\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t} else {\n\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} ","is_error":true,"tool_use_id":"toolu_01F6wPZcBHfjV2EfbBefiUgA"}]},"uuid":"1a101e9a-4017-49f3-a7ae-d33f4caa8cd7","timestamp":"2026-03-04T09:46:45.491Z","toolUseResult":"Error: String to replace not found in file.\nString: \t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\tif err == nil {\n\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t} else {\n\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}","sourceToolAssistantUUID":"c8587c24-db09-4ebe-a6ec-a60f6c712d32"} +{"parentUuid":"1a101e9a-4017-49f3-a7ae-d33f4caa8cd7","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_012VHrcTL8XTpoR1KqLB45MK","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01W5pfRRATWtFDvrSYUNVUQz","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go","offset":289,"limit":14},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":658,"cache_read_input_tokens":109336,"output_tokens":116,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":658,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhpx7nmxD6EbFWVTxGFn","type":"assistant","uuid":"912c6c73-8ee3-45c7-9574-a72e3bb3c1cf","timestamp":"2026-03-04T09:46:50.178Z"} +{"parentUuid":"912c6c73-8ee3-45c7-9574-a72e3bb3c1cf","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01W5pfRRATWtFDvrSYUNVUQz","toolUseID":"toolu_01W5pfRRATWtFDvrSYUNVUQz","timestamp":"2026-03-04T09:46:50.296Z","uuid":"859c2682-a2d8-4adc-9b37-01733c577b90"} +{"parentUuid":"912c6c73-8ee3-45c7-9574-a72e3bb3c1cf","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01W5pfRRATWtFDvrSYUNVUQz","type":"tool_result","content":" 289→\t\t\t\tusers++\n 290→\t\t\t\tif w.idpManager == EmbeddedType {\n 291→\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n 292→\t\t\t\t\tif err == nil {\n 293→\t\t\t\t\t\tif idpID == \"local\" {\n 294→\t\t\t\t\t\t\tlocalUsers++\n 295→\t\t\t\t\t\t} else {\n 296→\t\t\t\t\t\t\tidpUsers++\n 297→\t\t\t\t\t\t\tidpType := extractIdpType(idpID)\n 298→\t\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n 299→\t\t\t\t\t\t}\n 300→\t\t\t\t\t}\n 301→\t\t\t\t}\n 302→\t\t\t}"}]},"uuid":"ba6f25d7-0802-4426-8e9f-8f7d529f3aa0","timestamp":"2026-03-04T09:46:50.296Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go","content":"\t\t\t\tusers++\n\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}","numLines":14,"startLine":289,"totalLines":567}},"sourceToolAssistantUUID":"912c6c73-8ee3-45c7-9574-a72e3bb3c1cf"} +{"parentUuid":"ba6f25d7-0802-4426-8e9f-8f7d529f3aa0","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01Qpsx6zTYUt36EjQCjDu5y2","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01TAAHWp731vqoFHBYgv4LwB","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go","old_string":"\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\tif err == nil {\n\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t} else {\n\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}","new_string":"\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\tif err == nil {\n\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t} else {\n\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t}\n\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t}\n\t\t\t}"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":285,"cache_read_input_tokens":109994,"output_tokens":353,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":285,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhpxTvyn3waRKUHWjdwG","type":"assistant","uuid":"ffd80139-8cc1-4d83-ba5e-910ec2b1a740","timestamp":"2026-03-04T09:46:56.468Z"} +{"parentUuid":"ffd80139-8cc1-4d83-ba5e-910ec2b1a740","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"type":"tool_result","content":"String to replace not found in file.\nString: \t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\tif err == nil {\n\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t} else {\n\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} ","is_error":true,"tool_use_id":"toolu_01TAAHWp731vqoFHBYgv4LwB"}]},"uuid":"d7d67dfd-d931-4769-9ea5-3b1bd3bf482c","timestamp":"2026-03-04T09:46:56.471Z","toolUseResult":"Error: String to replace not found in file.\nString: \t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\tif err == nil {\n\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t} else {\n\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}","sourceToolAssistantUUID":"ffd80139-8cc1-4d83-ba5e-910ec2b1a740"} +{"parentUuid":"d7d67dfd-d931-4769-9ea5-3b1bd3bf482c","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01CpHWphyF1jU44xEMfLLm8A","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_017w4uJsP9NiT3R9mXh8RaiP","name":"Bash","input":{"command":"cat -A /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go | sed -n '290,301p'","description":"Show exact whitespace characters in the target lines"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":616,"cache_read_input_tokens":110279,"output_tokens":119,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":616,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhpxv94jxYsKrPdFTgeW","type":"assistant","uuid":"1ab737c1-df57-499c-9ec9-d90b6ef94fc3","timestamp":"2026-03-04T09:47:01.388Z"} +{"parentUuid":"1ab737c1-df57-499c-9ec9-d90b6ef94fc3","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_017w4uJsP9NiT3R9mXh8RaiP","type":"tool_result","content":"cat: illegal option -- A\nusage: cat [-belnstuv] [file ...]","is_error":false}]},"uuid":"2e73e65e-3fc4-4929-b471-5be458642740","timestamp":"2026-03-04T09:47:01.524Z","toolUseResult":{"stdout":"cat: illegal option -- A\nusage: cat [-belnstuv] [file ...]","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"1ab737c1-df57-499c-9ec9-d90b6ef94fc3"} +{"parentUuid":"2e73e65e-3fc4-4929-b471-5be458642740","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01VirmdARJCY6W3ELjUCdUrX","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01PjZLGnzzbBSygh74K9pVnL","name":"Bash","input":{"command":"sed -n '290,301p' /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go | cat -v | head -20","description":"Show raw bytes in target lines"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":151,"cache_read_input_tokens":110895,"output_tokens":120,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":151,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhpyHUxbAdhNHVhRz9sb","type":"assistant","uuid":"b73edc5e-0b5c-4599-8b50-d97083104085","timestamp":"2026-03-04T09:47:06.497Z"} +{"parentUuid":"b73edc5e-0b5c-4599-8b50-d97083104085","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01PjZLGnzzbBSygh74K9pVnL","type":"tool_result","content":"\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}","is_error":false}]},"uuid":"82b07ec2-784d-4c14-bad8-77fa6fda8d83","timestamp":"2026-03-04T09:47:06.636Z","toolUseResult":{"stdout":"\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"b73edc5e-0b5c-4599-8b50-d97083104085"} +{"parentUuid":"82b07ec2-784d-4c14-bad8-77fa6fda8d83","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_014gcKKcV2cDtzhbX1iTgmXK","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01UWrjDcSh9stGQUvHovuz2r","name":"Bash","input":{"command":"xxd /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go | grep -m1 \"idpManager\"","description":"Check if tabs or spaces"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":240,"cache_read_input_tokens":111046,"output_tokens":115,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":240,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhpz1wr3etKYfqUxLZAw","type":"assistant","uuid":"be946481-109b-4d21-aefc-51b96514dba6","timestamp":"2026-03-04T09:47:16.392Z"} +{"parentUuid":"be946481-109b-4d21-aefc-51b96514dba6","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01UWrjDcSh9stGQUvHovuz2r","type":"tool_result","content":"00000710: 670a 0969 6470 4d61 6e61 6765 7220 2073 g..idpManager s","is_error":false}]},"uuid":"46d9e8f9-bd61-4f4a-9cac-06c4d0cb3bce","timestamp":"2026-03-04T09:49:27.017Z","toolUseResult":{"stdout":"00000710: 670a 0969 6470 4d61 6e61 6765 7220 2073 g..idpManager s","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"be946481-109b-4d21-aefc-51b96514dba6"} +{"parentUuid":"46d9e8f9-bd61-4f4a-9cac-06c4d0cb3bce","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01NjY7sjCdu2AoUozZt26uYV","type":"message","role":"assistant","content":[{"type":"text","text":"Tabs. Let me use the exact content from the file:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":165,"cache_read_input_tokens":111286,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":165},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhqA2d4EJk1zPXHXxF9L","type":"assistant","uuid":"de43e85c-f7f6-4e48-b2c0-e380181b0cd1","timestamp":"2026-03-04T09:49:31.211Z"} +{"parentUuid":"de43e85c-f7f6-4e48-b2c0-e380181b0cd1","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01NjY7sjCdu2AoUozZt26uYV","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_011cN8cHySkJAPnimXcAXUM4","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go","old_string":"\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t} else {\n\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}","new_string":"\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t} else {\n\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t}\n\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\tembeddedIdpTypes[idpType]++"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":165,"cache_read_input_tokens":111286,"output_tokens":266,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":165,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqA2d4EJk1zPXHXxF9L","type":"assistant","uuid":"7bcb29da-e522-4c18-99ca-f2784a21468b","timestamp":"2026-03-04T09:49:32.984Z"} +{"parentUuid":"7bcb29da-e522-4c18-99ca-f2784a21468b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"type":"tool_result","content":"String to replace not found in file.\nString: \t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t} else {\n\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t} ","is_error":true,"tool_use_id":"toolu_011cN8cHySkJAPnimXcAXUM4"}]},"uuid":"2877e36d-a08c-45a9-85e3-e8cdc392456f","timestamp":"2026-03-04T09:49:32.989Z","toolUseResult":"Error: String to replace not found in file.\nString: \t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t} else {\n\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}","sourceToolAssistantUUID":"7bcb29da-e522-4c18-99ca-f2784a21468b"} +{"parentUuid":"2877e36d-a08c-45a9-85e3-e8cdc392456f","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_012BBnq3dBto5YPPV9bPYw5F","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01FK5p9AxqiwknqCgR1xmD6E","name":"Bash","input":{"command":"hexdump -C /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go | sed -n '/idpID == .local/,/embeddedIdpTypes/p' | head -20","description":"Show hex dump of target region"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":367,"cache_read_input_tokens":111451,"output_tokens":135,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":367,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqAoUKC4QwmgihAgzCA","type":"assistant","uuid":"fd0b7f31-7ce4-478a-86d1-355c1509f124","timestamp":"2026-03-04T09:49:42.666Z"} +{"parentUuid":"fd0b7f31-7ce4-478a-86d1-355c1509f124","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01FK5p9AxqiwknqCgR1xmD6E","type":"tool_result","content":"","is_error":false}]},"uuid":"e0d397b8-6f34-4d0f-b724-04518b8cd178","timestamp":"2026-03-04T09:49:44.797Z","toolUseResult":{"stdout":"","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"fd0b7f31-7ce4-478a-86d1-355c1509f124"} +{"parentUuid":"e0d397b8-6f34-4d0f-b724-04518b8cd178","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01JYEEUwKBJMEhnNmpdRJNBm","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_019CpkkZAM5MyH7NdFjeq6jp","name":"Bash","input":{"command":"awk 'NR>=293 && NR<=299' /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go | xxd","description":"Hex dump of exact lines 293-299"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":161,"cache_read_input_tokens":111818,"output_tokens":125,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":161,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqBLMBZRo5LdHhujHJK","type":"assistant","uuid":"d911cfcc-6aa6-484b-a559-b3f975a2af79","timestamp":"2026-03-04T09:49:49.182Z"} +{"parentUuid":"d911cfcc-6aa6-484b-a559-b3f975a2af79","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_019CpkkZAM5MyH7NdFjeq6jp","type":"tool_result","content":"00000000: 0909 0909 0909 6966 2069 6470 4944 203d ......if idpID =\n00000010: 3d20 226c 6f63 616c 2220 7b0a 0909 0909 = \"local\" {.....\n00000020: 0909 096c 6f63 616c 5573 6572 732b 2b0a ...localUsers++.\n00000030: 0909 0909 0909 7d20 656c 7365 207b 0a09 ......} else {..\n00000040: 0909 0909 0909 6964 7055 7365 7273 2b2b ......idpUsers++\n00000050: 0a09 0909 0909 0909 6964 7054 7970 6520 ........idpType \n00000060: 3a3d 2065 7874 7261 6374 4964 7054 7970 := extractIdpTyp\n00000070: 6528 6964 7049 4429 0a09 0909 0909 0909 e(idpID)........\n00000080: 656d 6265 6464 6564 4964 7054 7970 6573 embeddedIdpTypes\n00000090: 5b69 6470 5479 7065 5d2b 2b0a 0909 0909 [idpType]++.....\n000000a0: 0909 7d0a ..}.","is_error":false}]},"uuid":"de6bedf8-5e0f-4c04-abd6-2fd42ddb2301","timestamp":"2026-03-04T09:49:59.040Z","toolUseResult":{"stdout":"00000000: 0909 0909 0909 6966 2069 6470 4944 203d ......if idpID =\n00000010: 3d20 226c 6f63 616c 2220 7b0a 0909 0909 = \"local\" {.....\n00000020: 0909 096c 6f63 616c 5573 6572 732b 2b0a ...localUsers++.\n00000030: 0909 0909 0909 7d20 656c 7365 207b 0a09 ......} else {..\n00000040: 0909 0909 0909 6964 7055 7365 7273 2b2b ......idpUsers++\n00000050: 0a09 0909 0909 0909 6964 7054 7970 6520 ........idpType \n00000060: 3a3d 2065 7874 7261 6374 4964 7054 7970 := extractIdpTyp\n00000070: 6528 6964 7049 4429 0a09 0909 0909 0909 e(idpID)........\n00000080: 656d 6265 6464 6564 4964 7054 7970 6573 embeddedIdpTypes\n00000090: 5b69 6470 5479 7065 5d2b 2b0a 0909 0909 [idpType]++.....\n000000a0: 0909 7d0a ..}.","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"d911cfcc-6aa6-484b-a559-b3f975a2af79"} +{"parentUuid":"de6bedf8-5e0f-4c04-abd6-2fd42ddb2301","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_017jf1KJWEzeqboVfCCn77GM","type":"message","role":"assistant","content":[{"type":"text","text":"The file uses tabs. Let me write the edit using the exact content:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":537,"cache_read_input_tokens":111979,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":537},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhqCPrJrkPS7u8W8UG2D","type":"assistant","uuid":"8513a309-87e4-46f9-b0ae-63b28c7f31a7","timestamp":"2026-03-04T09:50:04.686Z"} +{"parentUuid":"8513a309-87e4-46f9-b0ae-63b28c7f31a7","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_017jf1KJWEzeqboVfCCn77GM","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01XGYgDGw5z18tkMEZp9sUk7","name":"Bash","input":{"command":"awk 'NR>=293 && NR<=299' /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go","description":"Print exact lines to match"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":537,"cache_read_input_tokens":111979,"output_tokens":133,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":537,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqCPrJrkPS7u8W8UG2D","type":"assistant","uuid":"7f73d904-b437-475d-b65a-d6a64b5093d0","timestamp":"2026-03-04T09:50:05.356Z"} +{"parentUuid":"7f73d904-b437-475d-b65a-d6a64b5093d0","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01XGYgDGw5z18tkMEZp9sUk7","type":"tool_result","content":"\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t\t}","is_error":false}]},"uuid":"d0bdf310-ea0e-44e0-ad06-438ef3f185ae","timestamp":"2026-03-04T09:50:07.732Z","toolUseResult":{"stdout":"\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t\t}","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"7f73d904-b437-475d-b65a-d6a64b5093d0"} +{"parentUuid":"d0bdf310-ea0e-44e0-ad06-438ef3f185ae","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_018xJVaEjyFK2C8deuZ5CTjE","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01CBBHZpZ86Webb4LPNXaEZZ","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go","old_string":"\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t\t}","new_string":"\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t}\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":202,"cache_read_input_tokens":112516,"output_tokens":233,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":202,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqD1ko9jzRtnTQWj4RW","type":"assistant","uuid":"264a6317-2fcb-4248-8455-bb1c2f548798","timestamp":"2026-03-04T09:50:13.094Z"} +{"type":"file-history-snapshot","messageId":"264a6317-2fcb-4248-8455-bb1c2f548798","snapshot":{"messageId":"af2b1ed5-da44-4083-a356-f7edb52c68b8","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v3","version":3,"backupTime":"2026-03-03T09:45:34.548Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v3","version":3,"backupTime":"2026-03-04T09:35:30.335Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v2","version":2,"backupTime":"2026-03-02T19:45:28.129Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/config.yaml.example":{"backupFileName":"ab8acefcb43dda84@v2","version":2,"backupTime":"2026-03-02T19:45:28.132Z"},"management/server/metrics/selfhosted.go":{"backupFileName":"e4e6cf8f05310807@v1","version":1,"backupTime":"2026-03-04T09:50:22.393Z"}},"timestamp":"2026-03-04T09:45:12.815Z"},"isSnapshotUpdate":true} +{"parentUuid":"264a6317-2fcb-4248-8455-bb1c2f548798","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_01CBBHZpZ86Webb4LPNXaEZZ","toolUseID":"toolu_01CBBHZpZ86Webb4LPNXaEZZ","timestamp":"2026-03-04T09:50:22.398Z","uuid":"5762f042-c7c1-44ec-9e8c-1d95d9e5d980"} +{"parentUuid":"264a6317-2fcb-4248-8455-bb1c2f548798","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01CBBHZpZ86Webb4LPNXaEZZ","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go has been updated successfully."}]},"uuid":"be4da50f-f284-4ff4-a020-95533ab774ae","timestamp":"2026-03-04T09:50:22.397Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go","oldString":"// Package metrics gather anonymous information about the usage of NetBird management\npackage metrics\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\tnbversion \"github.com/netbirdio/netbird/version\"\n)\n\nconst (\n\t// PayloadEvent identifies an event type\n\tPayloadEvent = \"self-hosted stats\"\n\t// payloadEndpoint metrics defaultEndpoint to send anonymous data\n\tpayloadEndpoint = \"https://metrics.netbird.io\"\n\t// defaultPushInterval default interval to push metrics\n\tdefaultPushInterval = 12 * time.Hour\n\t// requestTimeout http request timeout\n\trequestTimeout = 45 * time.Second\n\tEmbeddedType = \"embedded\"\n)\n\ntype getTokenResponse struct {\n\tPublicAPIToken string `json:\"public_api_token\"`\n}\n\ntype pushPayload struct {\n\tAPIKey string `json:\"api_key\"`\n\tDistinctID string `json:\"distinct_id\"`\n\tEvent string `json:\"event\"`\n\tProperties properties `json:\"properties\"`\n\tTimestamp time.Time `json:\"timestamp\"`\n}\n\n// properties metrics to push\ntype properties map[string]interface{}\n\n// DataSource metric data source\ntype DataSource interface {\n\tGetAllAccounts(ctx context.Context) []*types.Account\n\tGetStoreEngine() types.Engine\n\tGetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)\n}\n\n// ConnManager peer connection manager that holds state for current active connections\ntype ConnManager interface {\n\tGetAllConnectedPeers() map[string]struct{}\n}\n\n// Worker metrics collector and pusher\ntype Worker struct {\n\tctx context.Context\n\tid string\n\tidpManager string\n\tdataSource DataSource\n\tconnManager ConnManager\n\tstartupTime time.Time\n\tlastRun time.Time\n}\n\n// NewWorker returns a metrics worker\nfunc NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {\n\tcurrentTime := time.Now()\n\treturn &Worker{\n\t\tctx: ctx,\n\t\tid: id,\n\t\tidpManager: idpManager,\n\t\tdataSource: dataSource,\n\t\tconnManager: connManager,\n\t\tstartupTime: currentTime,\n\t\tlastRun: currentTime,\n\t}\n}\n\n// Run runs the metrics worker\nfunc (w *Worker) Run(ctx context.Context) {\n\tinterval := getMetricsInterval(ctx)\n\n\tpushTicker := time.NewTicker(interval)\n\tfor {\n\t\tselect {\n\t\tcase <-w.ctx.Done():\n\t\t\treturn\n\t\tcase <-pushTicker.C:\n\t\t\terr := w.sendMetrics(ctx)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithContext(ctx).Error(err)\n\t\t\t}\n\t\t\tw.lastRun = time.Now()\n\t\t}\n\t}\n}\n\nfunc getMetricsInterval(ctx context.Context) time.Duration {\n\tinterval := defaultPushInterval\n\tif os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") != \"\" {\n\t\tnewInterval, err := time.ParseDuration(os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") + \"s\")\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"unable to parse NETBIRD_METRICS_INTERVAL_IN_SECONDS, using default interval %v. Error: %v\", defaultPushInterval, err)\n\t\t} else {\n\t\t\tlog.WithContext(ctx).Infof(\"using NETBIRD_METRICS_INTERVAL_IN_SECONDS %s\", newInterval)\n\t\t\tinterval = newInterval\n\t\t}\n\t}\n\treturn interval\n}\n\nfunc (w *Worker) sendMetrics(ctx context.Context) error {\n\tapiKey, err := getAPIKey(w.ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpayload := w.generatePayload(ctx, apiKey)\n\n\tpayloadString, err := buildMetricsPayload(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thttpClient := http.Client{}\n\n\texportJobReq, cancelCTX, err := createPostRequest(w.ctx, payloadEndpoint+\"/capture/\", payloadString)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create metrics post request %v\", err)\n\t}\n\tdefer cancelCTX()\n\n\tjobResp, err := httpClient.Do(exportJobReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to push metrics %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = jobResp.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing update metrics response body: %v\", err)\n\t\t}\n\t}()\n\n\tif jobResp.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"unable to push anonymous metrics, got statusCode %d\", jobResp.StatusCode)\n\t}\n\n\tlog.WithContext(ctx).Infof(\"sent anonymous metrics, next push will happen in %s. \"+\n\t\t\"You can disable these metrics by running with flag --disable-anonymous-metrics,\"+\n\t\t\" see more information at https://docs.netbird.io/about-netbird/faq#why-and-what-are-the-anonymous-usage-metrics\", getMetricsInterval(ctx))\n\n\treturn nil\n}\n\nfunc (w *Worker) generatePayload(ctx context.Context, apiKey string) pushPayload {\n\tproperties := w.generateProperties(ctx)\n\n\treturn pushPayload{\n\t\tAPIKey: apiKey,\n\t\tDistinctID: w.id,\n\t\tEvent: PayloadEvent,\n\t\tProperties: properties,\n\t\tTimestamp: time.Now(),\n\t}\n}\n\nfunc (w *Worker) generateProperties(ctx context.Context) properties {\n\tvar (\n\t\tuptime float64\n\t\taccounts int\n\t\texpirationEnabled int\n\t\tusers int\n\t\tserviceUsers int\n\t\tpats int\n\t\tpeers int\n\t\tpeersSSHEnabled int\n\t\tsetupKeysUsage int\n\t\tephemeralPeersSKs int\n\t\tephemeralPeersSKUsage int\n\t\tactivePeersLastDay int\n\t\tactiveUserPeersLastDay int\n\t\tosPeers map[string]int\n\t\tactiveUsersLastDay map[string]struct{}\n\t\tuserPeers int\n\t\trules int\n\t\trulesProtocol map[string]int\n\t\trulesDirection map[string]int\n\t\trulesWithSrcPostureChecks int\n\t\tpostureChecks int\n\t\tgroups int\n\t\troutes int\n\t\troutesWithRGGroups int\n\t\tnetworks int\n\t\tnetworkResources int\n\t\tnetworkRouters int\n\t\tnetworkRoutersWithPG int\n\t\tnameservers int\n\t\tuiClient int\n\t\tversion string\n\t\tpeerActiveVersions []string\n\t\tosUIClients map[string]int\n\t\trosenpassEnabled int\n\t\tlocalUsers int\n\t\tidpUsers int\n\t\tembeddedIdpTypes map[string]int\n\t\tservices int\n\t\tservicesEnabled int\n\t\tservicesTargets int\n\t\tservicesStatusActive int\n\t\tservicesStatusPending int\n\t\tservicesStatusError int\n\t\tservicesTargetType map[string]int\n\t\tservicesAuthPassword int\n\t\tservicesAuthPin int\n\t\tservicesAuthOIDC int\n\t)\n\tstart := time.Now()\n\tmetricsProperties := make(properties)\n\tosPeers = make(map[string]int)\n\tosUIClients = make(map[string]int)\n\trulesProtocol = make(map[string]int)\n\trulesDirection = make(map[string]int)\n\tactiveUsersLastDay = make(map[string]struct{})\n\tembeddedIdpTypes = make(map[string]int)\n\tservicesTargetType = make(map[string]int)\n\tuptime = time.Since(w.startupTime).Seconds()\n\tconnections := w.connManager.GetAllConnectedPeers()\n\tversion = nbversion.NetbirdVersion()\n\n\tcustomDomains, customDomainsValidated, _ := w.dataSource.GetCustomDomainsCounts(ctx)\n\n\tfor _, account := range w.dataSource.GetAllAccounts(ctx) {\n\t\taccounts++\n\n\t\tif account.Settings.PeerLoginExpirationEnabled {\n\t\t\texpirationEnabled++\n\t\t}\n\n\t\tgroups += len(account.Groups)\n\t\tnetworks += len(account.Networks)\n\t\tnetworkResources += len(account.NetworkResources)\n\n\t\tnetworkRouters += len(account.NetworkRouters)\n\t\tfor _, router := range account.NetworkRouters {\n\t\t\tif len(router.PeerGroups) > 0 {\n\t\t\t\tnetworkRoutersWithPG++\n\t\t\t}\n\t\t}\n\n\t\troutes += len(account.Routes)\n\t\tfor _, route := range account.Routes {\n\t\t\tif len(route.PeerGroups) > 0 {\n\t\t\t\troutesWithRGGroups++\n\t\t\t}\n\t\t}\n\t\tnameservers += len(account.NameServerGroups)\n\n\t\tfor _, policy := range account.Policies {\n\t\t\tfor _, rule := range policy.Rules {\n\t\t\t\trules++\n\t\t\t\trulesProtocol[string(rule.Protocol)]++\n\t\t\t\tif rule.Bidirectional {\n\t\t\t\t\trulesDirection[\"bidirectional\"]++\n\t\t\t\t} else {\n\t\t\t\t\trulesDirection[\"oneway\"]++\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(policy.SourcePostureChecks) > 0 {\n\t\t\t\trulesWithSrcPostureChecks++\n\t\t\t}\n\t\t}\n\n\t\tpostureChecks += len(account.PostureChecks)\n\n\t\tfor _, user := range account.Users {\n\t\t\tif user.IsServiceUser {\n\t\t\t\tserviceUsers++\n\t\t\t} else {\n\t\t\t\tusers++\n\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpats += len(user.PATs)\n\t\t}\n\n\t\tfor _, key := range account.SetupKeys {\n\t\t\tsetupKeysUsage += key.UsedTimes\n\t\t\tif key.Ephemeral {\n\t\t\t\tephemeralPeersSKs++\n\t\t\t\tephemeralPeersSKUsage += key.UsedTimes\n\t\t\t}\n\t\t}\n\n\t\tfor _, peer := range account.Peers {\n\t\t\tpeers++\n\n\t\t\tif peer.SSHEnabled || peer.Meta.Flags.ServerSSHAllowed {\n\t\t\t\tpeersSSHEnabled++\n\t\t\t}\n\n\t\t\tif peer.Meta.Flags.RosenpassEnabled {\n\t\t\t\trosenpassEnabled++\n\t\t\t}\n\n\t\t\tif peer.UserID != \"\" {\n\t\t\t\tuserPeers++\n\t\t\t}\n\n\t\t\tosKey := strings.ToLower(fmt.Sprintf(\"peer_os_%s\", peer.Meta.GoOS))\n\t\t\tosCount := osPeers[osKey]\n\t\t\tosPeers[osKey] = osCount + 1\n\n\t\t\tif peer.Meta.UIVersion != \"\" {\n\t\t\t\tuiClient++\n\t\t\t\tuiOSKey := strings.ToLower(fmt.Sprintf(\"ui_client_os_%s\", peer.Meta.GoOS))\n\t\t\t\tosUICount := osUIClients[uiOSKey]\n\t\t\t\tosUIClients[uiOSKey] = osUICount + 1\n\t\t\t}\n\n\t\t\t_, connected := connections[peer.ID]\n\t\t\tif connected || peer.Status.LastSeen.After(w.lastRun) {\n\t\t\t\tactivePeersLastDay++\n\t\t\t\tif peer.UserID != \"\" {\n\t\t\t\t\tactiveUserPeersLastDay++\n\t\t\t\t\tactiveUsersLastDay[peer.UserID] = struct{}{}\n\t\t\t\t}\n\t\t\t\tosActiveKey := osKey + \"_active\"\n\t\t\t\tosActiveCount := osPeers[osActiveKey]\n\t\t\t\tosPeers[osActiveKey] = osActiveCount + 1\n\t\t\t\tpeerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)\n\t\t\t}\n\t\t}\n\n\t\tfor _, service := range account.Services {\n\t\t\tservices++\n\t\t\tif service.Enabled {\n\t\t\t\tservicesEnabled++\n\t\t\t}\n\t\t\tservicesTargets += len(service.Targets)\n\n\t\t\tswitch rpservice.Status(service.Meta.Status) {\n\t\t\tcase rpservice.StatusActive:\n\t\t\t\tservicesStatusActive++\n\t\t\tcase rpservice.StatusPending:\n\t\t\t\tservicesStatusPending++\n\t\t\tcase rpservice.StatusError, rpservice.StatusCertificateFailed, rpservice.StatusTunnelNotCreated:\n\t\t\t\tservicesStatusError++\n\t\t\t}\n\n\t\t\tfor _, target := range service.Targets {\n\t\t\t\tservicesTargetType[target.TargetType]++\n\t\t\t}\n\n\t\t\tif service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled {\n\t\t\t\tservicesAuthPassword++\n\t\t\t}\n\t\t\tif service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled {\n\t\t\t\tservicesAuthPin++\n\t\t\t}\n\t\t\tif service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled {\n\t\t\t\tservicesAuthOIDC++\n\t\t\t}\n\t\t}\n\t}\n\n\tminActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)\n\tmetricsProperties[\"uptime\"] = uptime\n\tmetricsProperties[\"accounts\"] = accounts\n\tmetricsProperties[\"users\"] = users\n\tmetricsProperties[\"service_users\"] = serviceUsers\n\tmetricsProperties[\"pats\"] = pats\n\tmetricsProperties[\"peers\"] = peers\n\tmetricsProperties[\"peers_ssh_enabled\"] = peersSSHEnabled\n\tmetricsProperties[\"peers_login_expiration_enabled\"] = expirationEnabled\n\tmetricsProperties[\"setup_keys_usage\"] = setupKeysUsage\n\tmetricsProperties[\"ephemeral_peers_setup_keys\"] = ephemeralPeersSKs\n\tmetricsProperties[\"ephemeral_peers_setup_keys_usage\"] = ephemeralPeersSKUsage\n\tmetricsProperties[\"active_peers_last_day\"] = activePeersLastDay\n\tmetricsProperties[\"active_user_peers_last_day\"] = activeUserPeersLastDay\n\tmetricsProperties[\"active_users_last_day\"] = len(activeUsersLastDay)\n\tmetricsProperties[\"user_peers\"] = userPeers\n\tmetricsProperties[\"rules\"] = rules\n\tmetricsProperties[\"rules_with_src_posture_checks\"] = rulesWithSrcPostureChecks\n\tmetricsProperties[\"posture_checks\"] = postureChecks\n\tmetricsProperties[\"groups\"] = groups\n\tmetricsProperties[\"networks\"] = networks\n\tmetricsProperties[\"network_resources\"] = networkResources\n\tmetricsProperties[\"network_routers\"] = networkRouters\n\tmetricsProperties[\"network_routers_with_groups\"] = networkRoutersWithPG\n\tmetricsProperties[\"routes\"] = routes\n\tmetricsProperties[\"routes_with_routing_groups\"] = routesWithRGGroups\n\tmetricsProperties[\"nameservers\"] = nameservers\n\tmetricsProperties[\"version\"] = version\n\tmetricsProperties[\"min_active_peer_version\"] = minActivePeerVersion\n\tmetricsProperties[\"max_active_peer_version\"] = maxActivePeerVersion\n\tmetricsProperties[\"ui_clients\"] = uiClient\n\tmetricsProperties[\"idp_manager\"] = w.idpManager\n\tmetricsProperties[\"store_engine\"] = w.dataSource.GetStoreEngine()\n\tmetricsProperties[\"rosenpass_enabled\"] = rosenpassEnabled\n\tmetricsProperties[\"local_users_count\"] = localUsers\n\tmetricsProperties[\"idp_users_count\"] = idpUsers\n\tmetricsProperties[\"embedded_idp_count\"] = len(embeddedIdpTypes)\n\n\tmetricsProperties[\"services\"] = services\n\tmetricsProperties[\"services_enabled\"] = servicesEnabled\n\tmetricsProperties[\"services_targets\"] = servicesTargets\n\tmetricsProperties[\"services_status_active\"] = servicesStatusActive\n\tmetricsProperties[\"services_status_pending\"] = servicesStatusPending\n\tmetricsProperties[\"services_status_error\"] = servicesStatusError\n\tmetricsProperties[\"services_auth_password\"] = servicesAuthPassword\n\tmetricsProperties[\"services_auth_pin\"] = servicesAuthPin\n\tmetricsProperties[\"services_auth_oidc\"] = servicesAuthOIDC\n\tmetricsProperties[\"custom_domains\"] = customDomains\n\tmetricsProperties[\"custom_domains_validated\"] = customDomainsValidated\n\n\tfor targetType, count := range servicesTargetType {\n\t\tmetricsProperties[\"services_target_type_\"+targetType] = count\n\t}\n\n\tfor idpType, count := range embeddedIdpTypes {\n\t\tmetricsProperties[\"embedded_idp_users_\"+idpType] = count\n\t}\n\n\tfor protocol, count := range rulesProtocol {\n\t\tmetricsProperties[\"rules_protocol_\"+protocol] = count\n\t}\n\n\tfor direction, count := range rulesDirection {\n\t\tmetricsProperties[\"rules_direction_\"+direction] = count\n\t}\n\n\tfor os, count := range osPeers {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tfor os, count := range osUIClients {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tmetricsProperties[\"metric_generation_time\"] = time.Since(start).Milliseconds()\n\n\treturn metricsProperties\n}\n\nfunc getAPIKey(ctx context.Context) (string, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\tdefer cancel()\n\n\thttpClient := http.Client{}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+\"/GetToken\", nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create request for metrics public api token %v\", err)\n\t}\n\tresponse, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to request metrics public api token %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing metrics token response body: %v\", err)\n\t\t}\n\t}()\n\n\tif response.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"unable to retrieve metrics token, statusCode %d\", response.StatusCode)\n\t}\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't get metrics token response; %v\", err)\n\t}\n\n\tvar tokenResponse getTokenResponse\n\n\terr = json.Unmarshal(body, &tokenResponse)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't parse metrics public api token; %v\", err)\n\t}\n\n\treturn tokenResponse.PublicAPIToken, nil\n}\n\nfunc buildMetricsPayload(payload pushPayload) (string, error) {\n\tstr, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to marshal metrics payload, got err: %v\", err)\n\t}\n\treturn string(str), nil\n}\n\nfunc createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, context.CancelFunc, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\treqURL := endpoint\n\n\tpayload := strings.NewReader(payloadStr)\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", reqURL, payload)\n\tif err != nil {\n\t\tcancel()\n\t\treturn nil, nil, err\n\t}\n\treq.Header.Add(\"content-type\", \"application/json\")\n\n\treturn req, cancel, nil\n}\n\n// extractIdpType extracts the IdP type from a Dex connector ID.\n// Connector IDs are formatted as \"- \" (e.g., \"okta-abc123\", \"zitadel-xyz\").\n// Returns the type prefix, or \"oidc\" if no known prefix is found.\nfunc extractIdpType(connectorID string) string {\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn \"oidc\"\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}\n\nfunc getMinMaxVersion(inputList []string) (string, string) {\n\tversions := make([]*version.Version, 0)\n\n\tfor _, raw := range inputList {\n\t\tif raw != \"\" && nbversion.SemverRegexp.MatchString(raw) {\n\t\t\tv, err := version.NewVersion(raw)\n\t\t\tif err == nil {\n\t\t\t\tversions = append(versions, v)\n\t\t\t}\n\t\t}\n\t}\n\n\ttargetIndex := 1\n\tl := len(versions)\n\n\tswitch l {\n\tcase 0:\n\t\treturn \"\", \"\"\n\tcase targetIndex:\n\t\tv := versions[targetIndex-1].String()\n\t\treturn v, v\n\tdefault:\n\t\tsort.Sort(version.Collection(versions))\n\t\treturn versions[targetIndex-1].String(), versions[l-1].String()\n\t}\n}","newString":"// Package metrics gather anonymous information about the usage of NetBird management\npackage metrics\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\tnbversion \"github.com/netbirdio/netbird/version\"\n)\n\nconst (\n\t// PayloadEvent identifies an event type\n\tPayloadEvent = \"self-hosted stats\"\n\t// payloadEndpoint metrics defaultEndpoint to send anonymous data\n\tpayloadEndpoint = \"https://metrics.netbird.io\"\n\t// defaultPushInterval default interval to push metrics\n\tdefaultPushInterval = 12 * time.Hour\n\t// requestTimeout http request timeout\n\trequestTimeout = 45 * time.Second\n\tEmbeddedType = \"embedded\"\n)\n\ntype getTokenResponse struct {\n\tPublicAPIToken string `json:\"public_api_token\"`\n}\n\ntype pushPayload struct {\n\tAPIKey string `json:\"api_key\"`\n\tDistinctID string `json:\"distinct_id\"`\n\tEvent string `json:\"event\"`\n\tProperties properties `json:\"properties\"`\n\tTimestamp time.Time `json:\"timestamp\"`\n}\n\n// properties metrics to push\ntype properties map[string]interface{}\n\n// DataSource metric data source\ntype DataSource interface {\n\tGetAllAccounts(ctx context.Context) []*types.Account\n\tGetStoreEngine() types.Engine\n\tGetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)\n}\n\n// ConnManager peer connection manager that holds state for current active connections\ntype ConnManager interface {\n\tGetAllConnectedPeers() map[string]struct{}\n}\n\n// Worker metrics collector and pusher\ntype Worker struct {\n\tctx context.Context\n\tid string\n\tidpManager string\n\tdataSource DataSource\n\tconnManager ConnManager\n\tstartupTime time.Time\n\tlastRun time.Time\n}\n\n// NewWorker returns a metrics worker\nfunc NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {\n\tcurrentTime := time.Now()\n\treturn &Worker{\n\t\tctx: ctx,\n\t\tid: id,\n\t\tidpManager: idpManager,\n\t\tdataSource: dataSource,\n\t\tconnManager: connManager,\n\t\tstartupTime: currentTime,\n\t\tlastRun: currentTime,\n\t}\n}\n\n// Run runs the metrics worker\nfunc (w *Worker) Run(ctx context.Context) {\n\tinterval := getMetricsInterval(ctx)\n\n\tpushTicker := time.NewTicker(interval)\n\tfor {\n\t\tselect {\n\t\tcase <-w.ctx.Done():\n\t\t\treturn\n\t\tcase <-pushTicker.C:\n\t\t\terr := w.sendMetrics(ctx)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithContext(ctx).Error(err)\n\t\t\t}\n\t\t\tw.lastRun = time.Now()\n\t\t}\n\t}\n}\n\nfunc getMetricsInterval(ctx context.Context) time.Duration {\n\tinterval := defaultPushInterval\n\tif os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") != \"\" {\n\t\tnewInterval, err := time.ParseDuration(os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") + \"s\")\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"unable to parse NETBIRD_METRICS_INTERVAL_IN_SECONDS, using default interval %v. Error: %v\", defaultPushInterval, err)\n\t\t} else {\n\t\t\tlog.WithContext(ctx).Infof(\"using NETBIRD_METRICS_INTERVAL_IN_SECONDS %s\", newInterval)\n\t\t\tinterval = newInterval\n\t\t}\n\t}\n\treturn interval\n}\n\nfunc (w *Worker) sendMetrics(ctx context.Context) error {\n\tapiKey, err := getAPIKey(w.ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpayload := w.generatePayload(ctx, apiKey)\n\n\tpayloadString, err := buildMetricsPayload(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thttpClient := http.Client{}\n\n\texportJobReq, cancelCTX, err := createPostRequest(w.ctx, payloadEndpoint+\"/capture/\", payloadString)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create metrics post request %v\", err)\n\t}\n\tdefer cancelCTX()\n\n\tjobResp, err := httpClient.Do(exportJobReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to push metrics %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = jobResp.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing update metrics response body: %v\", err)\n\t\t}\n\t}()\n\n\tif jobResp.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"unable to push anonymous metrics, got statusCode %d\", jobResp.StatusCode)\n\t}\n\n\tlog.WithContext(ctx).Infof(\"sent anonymous metrics, next push will happen in %s. \"+\n\t\t\"You can disable these metrics by running with flag --disable-anonymous-metrics,\"+\n\t\t\" see more information at https://docs.netbird.io/about-netbird/faq#why-and-what-are-the-anonymous-usage-metrics\", getMetricsInterval(ctx))\n\n\treturn nil\n}\n\nfunc (w *Worker) generatePayload(ctx context.Context, apiKey string) pushPayload {\n\tproperties := w.generateProperties(ctx)\n\n\treturn pushPayload{\n\t\tAPIKey: apiKey,\n\t\tDistinctID: w.id,\n\t\tEvent: PayloadEvent,\n\t\tProperties: properties,\n\t\tTimestamp: time.Now(),\n\t}\n}\n\nfunc (w *Worker) generateProperties(ctx context.Context) properties {\n\tvar (\n\t\tuptime float64\n\t\taccounts int\n\t\texpirationEnabled int\n\t\tusers int\n\t\tserviceUsers int\n\t\tpats int\n\t\tpeers int\n\t\tpeersSSHEnabled int\n\t\tsetupKeysUsage int\n\t\tephemeralPeersSKs int\n\t\tephemeralPeersSKUsage int\n\t\tactivePeersLastDay int\n\t\tactiveUserPeersLastDay int\n\t\tosPeers map[string]int\n\t\tactiveUsersLastDay map[string]struct{}\n\t\tuserPeers int\n\t\trules int\n\t\trulesProtocol map[string]int\n\t\trulesDirection map[string]int\n\t\trulesWithSrcPostureChecks int\n\t\tpostureChecks int\n\t\tgroups int\n\t\troutes int\n\t\troutesWithRGGroups int\n\t\tnetworks int\n\t\tnetworkResources int\n\t\tnetworkRouters int\n\t\tnetworkRoutersWithPG int\n\t\tnameservers int\n\t\tuiClient int\n\t\tversion string\n\t\tpeerActiveVersions []string\n\t\tosUIClients map[string]int\n\t\trosenpassEnabled int\n\t\tlocalUsers int\n\t\tidpUsers int\n\t\tembeddedIdpTypes map[string]int\n\t\tservices int\n\t\tservicesEnabled int\n\t\tservicesTargets int\n\t\tservicesStatusActive int\n\t\tservicesStatusPending int\n\t\tservicesStatusError int\n\t\tservicesTargetType map[string]int\n\t\tservicesAuthPassword int\n\t\tservicesAuthPin int\n\t\tservicesAuthOIDC int\n\t)\n\tstart := time.Now()\n\tmetricsProperties := make(properties)\n\tosPeers = make(map[string]int)\n\tosUIClients = make(map[string]int)\n\trulesProtocol = make(map[string]int)\n\trulesDirection = make(map[string]int)\n\tactiveUsersLastDay = make(map[string]struct{})\n\tembeddedIdpTypes = make(map[string]int)\n\tservicesTargetType = make(map[string]int)\n\tuptime = time.Since(w.startupTime).Seconds()\n\tconnections := w.connManager.GetAllConnectedPeers()\n\tversion = nbversion.NetbirdVersion()\n\n\tcustomDomains, customDomainsValidated, _ := w.dataSource.GetCustomDomainsCounts(ctx)\n\n\tfor _, account := range w.dataSource.GetAllAccounts(ctx) {\n\t\taccounts++\n\n\t\tif account.Settings.PeerLoginExpirationEnabled {\n\t\t\texpirationEnabled++\n\t\t}\n\n\t\tgroups += len(account.Groups)\n\t\tnetworks += len(account.Networks)\n\t\tnetworkResources += len(account.NetworkResources)\n\n\t\tnetworkRouters += len(account.NetworkRouters)\n\t\tfor _, router := range account.NetworkRouters {\n\t\t\tif len(router.PeerGroups) > 0 {\n\t\t\t\tnetworkRoutersWithPG++\n\t\t\t}\n\t\t}\n\n\t\troutes += len(account.Routes)\n\t\tfor _, route := range account.Routes {\n\t\t\tif len(route.PeerGroups) > 0 {\n\t\t\t\troutesWithRGGroups++\n\t\t\t}\n\t\t}\n\t\tnameservers += len(account.NameServerGroups)\n\n\t\tfor _, policy := range account.Policies {\n\t\t\tfor _, rule := range policy.Rules {\n\t\t\t\trules++\n\t\t\t\trulesProtocol[string(rule.Protocol)]++\n\t\t\t\tif rule.Bidirectional {\n\t\t\t\t\trulesDirection[\"bidirectional\"]++\n\t\t\t\t} else {\n\t\t\t\t\trulesDirection[\"oneway\"]++\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(policy.SourcePostureChecks) > 0 {\n\t\t\t\trulesWithSrcPostureChecks++\n\t\t\t}\n\t\t}\n\n\t\tpostureChecks += len(account.PostureChecks)\n\n\t\tfor _, user := range account.Users {\n\t\t\tif user.IsServiceUser {\n\t\t\t\tserviceUsers++\n\t\t\t} else {\n\t\t\t\tusers++\n\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t}\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpats += len(user.PATs)\n\t\t}\n\n\t\tfor _, key := range account.SetupKeys {\n\t\t\tsetupKeysUsage += key.UsedTimes\n\t\t\tif key.Ephemeral {\n\t\t\t\tephemeralPeersSKs++\n\t\t\t\tephemeralPeersSKUsage += key.UsedTimes\n\t\t\t}\n\t\t}\n\n\t\tfor _, peer := range account.Peers {\n\t\t\tpeers++\n\n\t\t\tif peer.SSHEnabled || peer.Meta.Flags.ServerSSHAllowed {\n\t\t\t\tpeersSSHEnabled++\n\t\t\t}\n\n\t\t\tif peer.Meta.Flags.RosenpassEnabled {\n\t\t\t\trosenpassEnabled++\n\t\t\t}\n\n\t\t\tif peer.UserID != \"\" {\n\t\t\t\tuserPeers++\n\t\t\t}\n\n\t\t\tosKey := strings.ToLower(fmt.Sprintf(\"peer_os_%s\", peer.Meta.GoOS))\n\t\t\tosCount := osPeers[osKey]\n\t\t\tosPeers[osKey] = osCount + 1\n\n\t\t\tif peer.Meta.UIVersion != \"\" {\n\t\t\t\tuiClient++\n\t\t\t\tuiOSKey := strings.ToLower(fmt.Sprintf(\"ui_client_os_%s\", peer.Meta.GoOS))\n\t\t\t\tosUICount := osUIClients[uiOSKey]\n\t\t\t\tosUIClients[uiOSKey] = osUICount + 1\n\t\t\t}\n\n\t\t\t_, connected := connections[peer.ID]\n\t\t\tif connected || peer.Status.LastSeen.After(w.lastRun) {\n\t\t\t\tactivePeersLastDay++\n\t\t\t\tif peer.UserID != \"\" {\n\t\t\t\t\tactiveUserPeersLastDay++\n\t\t\t\t\tactiveUsersLastDay[peer.UserID] = struct{}{}\n\t\t\t\t}\n\t\t\t\tosActiveKey := osKey + \"_active\"\n\t\t\t\tosActiveCount := osPeers[osActiveKey]\n\t\t\t\tosPeers[osActiveKey] = osActiveCount + 1\n\t\t\t\tpeerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)\n\t\t\t}\n\t\t}\n\n\t\tfor _, service := range account.Services {\n\t\t\tservices++\n\t\t\tif service.Enabled {\n\t\t\t\tservicesEnabled++\n\t\t\t}\n\t\t\tservicesTargets += len(service.Targets)\n\n\t\t\tswitch rpservice.Status(service.Meta.Status) {\n\t\t\tcase rpservice.StatusActive:\n\t\t\t\tservicesStatusActive++\n\t\t\tcase rpservice.StatusPending:\n\t\t\t\tservicesStatusPending++\n\t\t\tcase rpservice.StatusError, rpservice.StatusCertificateFailed, rpservice.StatusTunnelNotCreated:\n\t\t\t\tservicesStatusError++\n\t\t\t}\n\n\t\t\tfor _, target := range service.Targets {\n\t\t\t\tservicesTargetType[target.TargetType]++\n\t\t\t}\n\n\t\t\tif service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled {\n\t\t\t\tservicesAuthPassword++\n\t\t\t}\n\t\t\tif service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled {\n\t\t\t\tservicesAuthPin++\n\t\t\t}\n\t\t\tif service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled {\n\t\t\t\tservicesAuthOIDC++\n\t\t\t}\n\t\t}\n\t}\n\n\tminActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)\n\tmetricsProperties[\"uptime\"] = uptime\n\tmetricsProperties[\"accounts\"] = accounts\n\tmetricsProperties[\"users\"] = users\n\tmetricsProperties[\"service_users\"] = serviceUsers\n\tmetricsProperties[\"pats\"] = pats\n\tmetricsProperties[\"peers\"] = peers\n\tmetricsProperties[\"peers_ssh_enabled\"] = peersSSHEnabled\n\tmetricsProperties[\"peers_login_expiration_enabled\"] = expirationEnabled\n\tmetricsProperties[\"setup_keys_usage\"] = setupKeysUsage\n\tmetricsProperties[\"ephemeral_peers_setup_keys\"] = ephemeralPeersSKs\n\tmetricsProperties[\"ephemeral_peers_setup_keys_usage\"] = ephemeralPeersSKUsage\n\tmetricsProperties[\"active_peers_last_day\"] = activePeersLastDay\n\tmetricsProperties[\"active_user_peers_last_day\"] = activeUserPeersLastDay\n\tmetricsProperties[\"active_users_last_day\"] = len(activeUsersLastDay)\n\tmetricsProperties[\"user_peers\"] = userPeers\n\tmetricsProperties[\"rules\"] = rules\n\tmetricsProperties[\"rules_with_src_posture_checks\"] = rulesWithSrcPostureChecks\n\tmetricsProperties[\"posture_checks\"] = postureChecks\n\tmetricsProperties[\"groups\"] = groups\n\tmetricsProperties[\"networks\"] = networks\n\tmetricsProperties[\"network_resources\"] = networkResources\n\tmetricsProperties[\"network_routers\"] = networkRouters\n\tmetricsProperties[\"network_routers_with_groups\"] = networkRoutersWithPG\n\tmetricsProperties[\"routes\"] = routes\n\tmetricsProperties[\"routes_with_routing_groups\"] = routesWithRGGroups\n\tmetricsProperties[\"nameservers\"] = nameservers\n\tmetricsProperties[\"version\"] = version\n\tmetricsProperties[\"min_active_peer_version\"] = minActivePeerVersion\n\tmetricsProperties[\"max_active_peer_version\"] = maxActivePeerVersion\n\tmetricsProperties[\"ui_clients\"] = uiClient\n\tmetricsProperties[\"idp_manager\"] = w.idpManager\n\tmetricsProperties[\"store_engine\"] = w.dataSource.GetStoreEngine()\n\tmetricsProperties[\"rosenpass_enabled\"] = rosenpassEnabled\n\tmetricsProperties[\"local_users_count\"] = localUsers\n\tmetricsProperties[\"idp_users_count\"] = idpUsers\n\tmetricsProperties[\"embedded_idp_count\"] = len(embeddedIdpTypes)\n\n\tmetricsProperties[\"services\"] = services\n\tmetricsProperties[\"services_enabled\"] = servicesEnabled\n\tmetricsProperties[\"services_targets\"] = servicesTargets\n\tmetricsProperties[\"services_status_active\"] = servicesStatusActive\n\tmetricsProperties[\"services_status_pending\"] = servicesStatusPending\n\tmetricsProperties[\"services_status_error\"] = servicesStatusError\n\tmetricsProperties[\"services_auth_password\"] = servicesAuthPassword\n\tmetricsProperties[\"services_auth_pin\"] = servicesAuthPin\n\tmetricsProperties[\"services_auth_oidc\"] = servicesAuthOIDC\n\tmetricsProperties[\"custom_domains\"] = customDomains\n\tmetricsProperties[\"custom_domains_validated\"] = customDomainsValidated\n\n\tfor targetType, count := range servicesTargetType {\n\t\tmetricsProperties[\"services_target_type_\"+targetType] = count\n\t}\n\n\tfor idpType, count := range embeddedIdpTypes {\n\t\tmetricsProperties[\"embedded_idp_users_\"+idpType] = count\n\t}\n\n\tfor protocol, count := range rulesProtocol {\n\t\tmetricsProperties[\"rules_protocol_\"+protocol] = count\n\t}\n\n\tfor direction, count := range rulesDirection {\n\t\tmetricsProperties[\"rules_direction_\"+direction] = count\n\t}\n\n\tfor os, count := range osPeers {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tfor os, count := range osUIClients {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tmetricsProperties[\"metric_generation_time\"] = time.Since(start).Milliseconds()\n\n\treturn metricsProperties\n}\n\nfunc getAPIKey(ctx context.Context) (string, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\tdefer cancel()\n\n\thttpClient := http.Client{}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+\"/GetToken\", nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create request for metrics public api token %v\", err)\n\t}\n\tresponse, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to request metrics public api token %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing metrics token response body: %v\", err)\n\t\t}\n\t}()\n\n\tif response.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"unable to retrieve metrics token, statusCode %d\", response.StatusCode)\n\t}\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't get metrics token response; %v\", err)\n\t}\n\n\tvar tokenResponse getTokenResponse\n\n\terr = json.Unmarshal(body, &tokenResponse)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't parse metrics public api token; %v\", err)\n\t}\n\n\treturn tokenResponse.PublicAPIToken, nil\n}\n\nfunc buildMetricsPayload(payload pushPayload) (string, error) {\n\tstr, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to marshal metrics payload, got err: %v\", err)\n\t}\n\treturn string(str), nil\n}\n\nfunc createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, context.CancelFunc, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\treqURL := endpoint\n\n\tpayload := strings.NewReader(payloadStr)\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", reqURL, payload)\n\tif err != nil {\n\t\tcancel()\n\t\treturn nil, nil, err\n\t}\n\treq.Header.Add(\"content-type\", \"application/json\")\n\n\treturn req, cancel, nil\n}\n\n// extractIdpType extracts the IdP type from a Dex connector ID.\n// Connector IDs are formatted as \" - \" (e.g., \"okta-abc123\", \"zitadel-xyz\").\n// Returns the type prefix, or \"oidc\" if no known prefix is found.\nfunc extractIdpType(connectorID string) string {\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn \"oidc\"\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}\n\nfunc getMinMaxVersion(inputList []string) (string, string) {\n\tversions := make([]*version.Version, 0)\n\n\tfor _, raw := range inputList {\n\t\tif raw != \"\" && nbversion.SemverRegexp.MatchString(raw) {\n\t\t\tv, err := version.NewVersion(raw)\n\t\t\tif err == nil {\n\t\t\t\tversions = append(versions, v)\n\t\t\t}\n\t\t}\n\t}\n\n\ttargetIndex := 1\n\tl := len(versions)\n\n\tswitch l {\n\tcase 0:\n\t\treturn \"\", \"\"\n\tcase targetIndex:\n\t\tv := versions[targetIndex-1].String()\n\t\treturn v, v\n\tdefault:\n\t\tsort.Sort(version.Collection(versions))\n\t\treturn versions[targetIndex-1].String(), versions[l-1].String()\n\t}\n}","originalFile":"// Package metrics gather anonymous information about the usage of NetBird management\npackage metrics\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\tnbversion \"github.com/netbirdio/netbird/version\"\n)\n\nconst (\n\t// PayloadEvent identifies an event type\n\tPayloadEvent = \"self-hosted stats\"\n\t// payloadEndpoint metrics defaultEndpoint to send anonymous data\n\tpayloadEndpoint = \"https://metrics.netbird.io\"\n\t// defaultPushInterval default interval to push metrics\n\tdefaultPushInterval = 12 * time.Hour\n\t// requestTimeout http request timeout\n\trequestTimeout = 45 * time.Second\n\tEmbeddedType = \"embedded\"\n)\n\ntype getTokenResponse struct {\n\tPublicAPIToken string `json:\"public_api_token\"`\n}\n\ntype pushPayload struct {\n\tAPIKey string `json:\"api_key\"`\n\tDistinctID string `json:\"distinct_id\"`\n\tEvent string `json:\"event\"`\n\tProperties properties `json:\"properties\"`\n\tTimestamp time.Time `json:\"timestamp\"`\n}\n\n// properties metrics to push\ntype properties map[string]interface{}\n\n// DataSource metric data source\ntype DataSource interface {\n\tGetAllAccounts(ctx context.Context) []*types.Account\n\tGetStoreEngine() types.Engine\n\tGetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)\n}\n\n// ConnManager peer connection manager that holds state for current active connections\ntype ConnManager interface {\n\tGetAllConnectedPeers() map[string]struct{}\n}\n\n// Worker metrics collector and pusher\ntype Worker struct {\n\tctx context.Context\n\tid string\n\tidpManager string\n\tdataSource DataSource\n\tconnManager ConnManager\n\tstartupTime time.Time\n\tlastRun time.Time\n}\n\n// NewWorker returns a metrics worker\nfunc NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {\n\tcurrentTime := time.Now()\n\treturn &Worker{\n\t\tctx: ctx,\n\t\tid: id,\n\t\tidpManager: idpManager,\n\t\tdataSource: dataSource,\n\t\tconnManager: connManager,\n\t\tstartupTime: currentTime,\n\t\tlastRun: currentTime,\n\t}\n}\n\n// Run runs the metrics worker\nfunc (w *Worker) Run(ctx context.Context) {\n\tinterval := getMetricsInterval(ctx)\n\n\tpushTicker := time.NewTicker(interval)\n\tfor {\n\t\tselect {\n\t\tcase <-w.ctx.Done():\n\t\t\treturn\n\t\tcase <-pushTicker.C:\n\t\t\terr := w.sendMetrics(ctx)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithContext(ctx).Error(err)\n\t\t\t}\n\t\t\tw.lastRun = time.Now()\n\t\t}\n\t}\n}\n\nfunc getMetricsInterval(ctx context.Context) time.Duration {\n\tinterval := defaultPushInterval\n\tif os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") != \"\" {\n\t\tnewInterval, err := time.ParseDuration(os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") + \"s\")\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"unable to parse NETBIRD_METRICS_INTERVAL_IN_SECONDS, using default interval %v. Error: %v\", defaultPushInterval, err)\n\t\t} else {\n\t\t\tlog.WithContext(ctx).Infof(\"using NETBIRD_METRICS_INTERVAL_IN_SECONDS %s\", newInterval)\n\t\t\tinterval = newInterval\n\t\t}\n\t}\n\treturn interval\n}\n\nfunc (w *Worker) sendMetrics(ctx context.Context) error {\n\tapiKey, err := getAPIKey(w.ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpayload := w.generatePayload(ctx, apiKey)\n\n\tpayloadString, err := buildMetricsPayload(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thttpClient := http.Client{}\n\n\texportJobReq, cancelCTX, err := createPostRequest(w.ctx, payloadEndpoint+\"/capture/\", payloadString)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create metrics post request %v\", err)\n\t}\n\tdefer cancelCTX()\n\n\tjobResp, err := httpClient.Do(exportJobReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to push metrics %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = jobResp.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing update metrics response body: %v\", err)\n\t\t}\n\t}()\n\n\tif jobResp.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"unable to push anonymous metrics, got statusCode %d\", jobResp.StatusCode)\n\t}\n\n\tlog.WithContext(ctx).Infof(\"sent anonymous metrics, next push will happen in %s. \"+\n\t\t\"You can disable these metrics by running with flag --disable-anonymous-metrics,\"+\n\t\t\" see more information at https://docs.netbird.io/about-netbird/faq#why-and-what-are-the-anonymous-usage-metrics\", getMetricsInterval(ctx))\n\n\treturn nil\n}\n\nfunc (w *Worker) generatePayload(ctx context.Context, apiKey string) pushPayload {\n\tproperties := w.generateProperties(ctx)\n\n\treturn pushPayload{\n\t\tAPIKey: apiKey,\n\t\tDistinctID: w.id,\n\t\tEvent: PayloadEvent,\n\t\tProperties: properties,\n\t\tTimestamp: time.Now(),\n\t}\n}\n\nfunc (w *Worker) generateProperties(ctx context.Context) properties {\n\tvar (\n\t\tuptime float64\n\t\taccounts int\n\t\texpirationEnabled int\n\t\tusers int\n\t\tserviceUsers int\n\t\tpats int\n\t\tpeers int\n\t\tpeersSSHEnabled int\n\t\tsetupKeysUsage int\n\t\tephemeralPeersSKs int\n\t\tephemeralPeersSKUsage int\n\t\tactivePeersLastDay int\n\t\tactiveUserPeersLastDay int\n\t\tosPeers map[string]int\n\t\tactiveUsersLastDay map[string]struct{}\n\t\tuserPeers int\n\t\trules int\n\t\trulesProtocol map[string]int\n\t\trulesDirection map[string]int\n\t\trulesWithSrcPostureChecks int\n\t\tpostureChecks int\n\t\tgroups int\n\t\troutes int\n\t\troutesWithRGGroups int\n\t\tnetworks int\n\t\tnetworkResources int\n\t\tnetworkRouters int\n\t\tnetworkRoutersWithPG int\n\t\tnameservers int\n\t\tuiClient int\n\t\tversion string\n\t\tpeerActiveVersions []string\n\t\tosUIClients map[string]int\n\t\trosenpassEnabled int\n\t\tlocalUsers int\n\t\tidpUsers int\n\t\tembeddedIdpTypes map[string]int\n\t\tservices int\n\t\tservicesEnabled int\n\t\tservicesTargets int\n\t\tservicesStatusActive int\n\t\tservicesStatusPending int\n\t\tservicesStatusError int\n\t\tservicesTargetType map[string]int\n\t\tservicesAuthPassword int\n\t\tservicesAuthPin int\n\t\tservicesAuthOIDC int\n\t)\n\tstart := time.Now()\n\tmetricsProperties := make(properties)\n\tosPeers = make(map[string]int)\n\tosUIClients = make(map[string]int)\n\trulesProtocol = make(map[string]int)\n\trulesDirection = make(map[string]int)\n\tactiveUsersLastDay = make(map[string]struct{})\n\tembeddedIdpTypes = make(map[string]int)\n\tservicesTargetType = make(map[string]int)\n\tuptime = time.Since(w.startupTime).Seconds()\n\tconnections := w.connManager.GetAllConnectedPeers()\n\tversion = nbversion.NetbirdVersion()\n\n\tcustomDomains, customDomainsValidated, _ := w.dataSource.GetCustomDomainsCounts(ctx)\n\n\tfor _, account := range w.dataSource.GetAllAccounts(ctx) {\n\t\taccounts++\n\n\t\tif account.Settings.PeerLoginExpirationEnabled {\n\t\t\texpirationEnabled++\n\t\t}\n\n\t\tgroups += len(account.Groups)\n\t\tnetworks += len(account.Networks)\n\t\tnetworkResources += len(account.NetworkResources)\n\n\t\tnetworkRouters += len(account.NetworkRouters)\n\t\tfor _, router := range account.NetworkRouters {\n\t\t\tif len(router.PeerGroups) > 0 {\n\t\t\t\tnetworkRoutersWithPG++\n\t\t\t}\n\t\t}\n\n\t\troutes += len(account.Routes)\n\t\tfor _, route := range account.Routes {\n\t\t\tif len(route.PeerGroups) > 0 {\n\t\t\t\troutesWithRGGroups++\n\t\t\t}\n\t\t}\n\t\tnameservers += len(account.NameServerGroups)\n\n\t\tfor _, policy := range account.Policies {\n\t\t\tfor _, rule := range policy.Rules {\n\t\t\t\trules++\n\t\t\t\trulesProtocol[string(rule.Protocol)]++\n\t\t\t\tif rule.Bidirectional {\n\t\t\t\t\trulesDirection[\"bidirectional\"]++\n\t\t\t\t} else {\n\t\t\t\t\trulesDirection[\"oneway\"]++\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(policy.SourcePostureChecks) > 0 {\n\t\t\t\trulesWithSrcPostureChecks++\n\t\t\t}\n\t\t}\n\n\t\tpostureChecks += len(account.PostureChecks)\n\n\t\tfor _, user := range account.Users {\n\t\t\tif user.IsServiceUser {\n\t\t\t\tserviceUsers++\n\t\t\t} else {\n\t\t\t\tusers++\n\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpats += len(user.PATs)\n\t\t}\n\n\t\tfor _, key := range account.SetupKeys {\n\t\t\tsetupKeysUsage += key.UsedTimes\n\t\t\tif key.Ephemeral {\n\t\t\t\tephemeralPeersSKs++\n\t\t\t\tephemeralPeersSKUsage += key.UsedTimes\n\t\t\t}\n\t\t}\n\n\t\tfor _, peer := range account.Peers {\n\t\t\tpeers++\n\n\t\t\tif peer.SSHEnabled || peer.Meta.Flags.ServerSSHAllowed {\n\t\t\t\tpeersSSHEnabled++\n\t\t\t}\n\n\t\t\tif peer.Meta.Flags.RosenpassEnabled {\n\t\t\t\trosenpassEnabled++\n\t\t\t}\n\n\t\t\tif peer.UserID != \"\" {\n\t\t\t\tuserPeers++\n\t\t\t}\n\n\t\t\tosKey := strings.ToLower(fmt.Sprintf(\"peer_os_%s\", peer.Meta.GoOS))\n\t\t\tosCount := osPeers[osKey]\n\t\t\tosPeers[osKey] = osCount + 1\n\n\t\t\tif peer.Meta.UIVersion != \"\" {\n\t\t\t\tuiClient++\n\t\t\t\tuiOSKey := strings.ToLower(fmt.Sprintf(\"ui_client_os_%s\", peer.Meta.GoOS))\n\t\t\t\tosUICount := osUIClients[uiOSKey]\n\t\t\t\tosUIClients[uiOSKey] = osUICount + 1\n\t\t\t}\n\n\t\t\t_, connected := connections[peer.ID]\n\t\t\tif connected || peer.Status.LastSeen.After(w.lastRun) {\n\t\t\t\tactivePeersLastDay++\n\t\t\t\tif peer.UserID != \"\" {\n\t\t\t\t\tactiveUserPeersLastDay++\n\t\t\t\t\tactiveUsersLastDay[peer.UserID] = struct{}{}\n\t\t\t\t}\n\t\t\t\tosActiveKey := osKey + \"_active\"\n\t\t\t\tosActiveCount := osPeers[osActiveKey]\n\t\t\t\tosPeers[osActiveKey] = osActiveCount + 1\n\t\t\t\tpeerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)\n\t\t\t}\n\t\t}\n\n\t\tfor _, service := range account.Services {\n\t\t\tservices++\n\t\t\tif service.Enabled {\n\t\t\t\tservicesEnabled++\n\t\t\t}\n\t\t\tservicesTargets += len(service.Targets)\n\n\t\t\tswitch rpservice.Status(service.Meta.Status) {\n\t\t\tcase rpservice.StatusActive:\n\t\t\t\tservicesStatusActive++\n\t\t\tcase rpservice.StatusPending:\n\t\t\t\tservicesStatusPending++\n\t\t\tcase rpservice.StatusError, rpservice.StatusCertificateFailed, rpservice.StatusTunnelNotCreated:\n\t\t\t\tservicesStatusError++\n\t\t\t}\n\n\t\t\tfor _, target := range service.Targets {\n\t\t\t\tservicesTargetType[target.TargetType]++\n\t\t\t}\n\n\t\t\tif service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled {\n\t\t\t\tservicesAuthPassword++\n\t\t\t}\n\t\t\tif service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled {\n\t\t\t\tservicesAuthPin++\n\t\t\t}\n\t\t\tif service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled {\n\t\t\t\tservicesAuthOIDC++\n\t\t\t}\n\t\t}\n\t}\n\n\tminActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)\n\tmetricsProperties[\"uptime\"] = uptime\n\tmetricsProperties[\"accounts\"] = accounts\n\tmetricsProperties[\"users\"] = users\n\tmetricsProperties[\"service_users\"] = serviceUsers\n\tmetricsProperties[\"pats\"] = pats\n\tmetricsProperties[\"peers\"] = peers\n\tmetricsProperties[\"peers_ssh_enabled\"] = peersSSHEnabled\n\tmetricsProperties[\"peers_login_expiration_enabled\"] = expirationEnabled\n\tmetricsProperties[\"setup_keys_usage\"] = setupKeysUsage\n\tmetricsProperties[\"ephemeral_peers_setup_keys\"] = ephemeralPeersSKs\n\tmetricsProperties[\"ephemeral_peers_setup_keys_usage\"] = ephemeralPeersSKUsage\n\tmetricsProperties[\"active_peers_last_day\"] = activePeersLastDay\n\tmetricsProperties[\"active_user_peers_last_day\"] = activeUserPeersLastDay\n\tmetricsProperties[\"active_users_last_day\"] = len(activeUsersLastDay)\n\tmetricsProperties[\"user_peers\"] = userPeers\n\tmetricsProperties[\"rules\"] = rules\n\tmetricsProperties[\"rules_with_src_posture_checks\"] = rulesWithSrcPostureChecks\n\tmetricsProperties[\"posture_checks\"] = postureChecks\n\tmetricsProperties[\"groups\"] = groups\n\tmetricsProperties[\"networks\"] = networks\n\tmetricsProperties[\"network_resources\"] = networkResources\n\tmetricsProperties[\"network_routers\"] = networkRouters\n\tmetricsProperties[\"network_routers_with_groups\"] = networkRoutersWithPG\n\tmetricsProperties[\"routes\"] = routes\n\tmetricsProperties[\"routes_with_routing_groups\"] = routesWithRGGroups\n\tmetricsProperties[\"nameservers\"] = nameservers\n\tmetricsProperties[\"version\"] = version\n\tmetricsProperties[\"min_active_peer_version\"] = minActivePeerVersion\n\tmetricsProperties[\"max_active_peer_version\"] = maxActivePeerVersion\n\tmetricsProperties[\"ui_clients\"] = uiClient\n\tmetricsProperties[\"idp_manager\"] = w.idpManager\n\tmetricsProperties[\"store_engine\"] = w.dataSource.GetStoreEngine()\n\tmetricsProperties[\"rosenpass_enabled\"] = rosenpassEnabled\n\tmetricsProperties[\"local_users_count\"] = localUsers\n\tmetricsProperties[\"idp_users_count\"] = idpUsers\n\tmetricsProperties[\"embedded_idp_count\"] = len(embeddedIdpTypes)\n\n\tmetricsProperties[\"services\"] = services\n\tmetricsProperties[\"services_enabled\"] = servicesEnabled\n\tmetricsProperties[\"services_targets\"] = servicesTargets\n\tmetricsProperties[\"services_status_active\"] = servicesStatusActive\n\tmetricsProperties[\"services_status_pending\"] = servicesStatusPending\n\tmetricsProperties[\"services_status_error\"] = servicesStatusError\n\tmetricsProperties[\"services_auth_password\"] = servicesAuthPassword\n\tmetricsProperties[\"services_auth_pin\"] = servicesAuthPin\n\tmetricsProperties[\"services_auth_oidc\"] = servicesAuthOIDC\n\tmetricsProperties[\"custom_domains\"] = customDomains\n\tmetricsProperties[\"custom_domains_validated\"] = customDomainsValidated\n\n\tfor targetType, count := range servicesTargetType {\n\t\tmetricsProperties[\"services_target_type_\"+targetType] = count\n\t}\n\n\tfor idpType, count := range embeddedIdpTypes {\n\t\tmetricsProperties[\"embedded_idp_users_\"+idpType] = count\n\t}\n\n\tfor protocol, count := range rulesProtocol {\n\t\tmetricsProperties[\"rules_protocol_\"+protocol] = count\n\t}\n\n\tfor direction, count := range rulesDirection {\n\t\tmetricsProperties[\"rules_direction_\"+direction] = count\n\t}\n\n\tfor os, count := range osPeers {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tfor os, count := range osUIClients {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tmetricsProperties[\"metric_generation_time\"] = time.Since(start).Milliseconds()\n\n\treturn metricsProperties\n}\n\nfunc getAPIKey(ctx context.Context) (string, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\tdefer cancel()\n\n\thttpClient := http.Client{}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+\"/GetToken\", nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create request for metrics public api token %v\", err)\n\t}\n\tresponse, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to request metrics public api token %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing metrics token response body: %v\", err)\n\t\t}\n\t}()\n\n\tif response.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"unable to retrieve metrics token, statusCode %d\", response.StatusCode)\n\t}\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't get metrics token response; %v\", err)\n\t}\n\n\tvar tokenResponse getTokenResponse\n\n\terr = json.Unmarshal(body, &tokenResponse)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't parse metrics public api token; %v\", err)\n\t}\n\n\treturn tokenResponse.PublicAPIToken, nil\n}\n\nfunc buildMetricsPayload(payload pushPayload) (string, error) {\n\tstr, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to marshal metrics payload, got err: %v\", err)\n\t}\n\treturn string(str), nil\n}\n\nfunc createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, context.CancelFunc, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\treqURL := endpoint\n\n\tpayload := strings.NewReader(payloadStr)\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", reqURL, payload)\n\tif err != nil {\n\t\tcancel()\n\t\treturn nil, nil, err\n\t}\n\treq.Header.Add(\"content-type\", \"application/json\")\n\n\treturn req, cancel, nil\n}\n\n// extractIdpType extracts the IdP type from a Dex connector ID.\n// Connector IDs are formatted as \" - \" (e.g., \"okta-abc123\", \"zitadel-xyz\").\n// Returns the type prefix, or \"oidc\" if no known prefix is found.\nfunc extractIdpType(connectorID string) string {\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn \"oidc\"\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}\n\nfunc getMinMaxVersion(inputList []string) (string, string) {\n\tversions := make([]*version.Version, 0)\n\n\tfor _, raw := range inputList {\n\t\tif raw != \"\" && nbversion.SemverRegexp.MatchString(raw) {\n\t\t\tv, err := version.NewVersion(raw)\n\t\t\tif err == nil {\n\t\t\t\tversions = append(versions, v)\n\t\t\t}\n\t\t}\n\t}\n\n\ttargetIndex := 1\n\tl := len(versions)\n\n\tswitch l {\n\tcase 0:\n\t\treturn \"\", \"\"\n\tcase targetIndex:\n\t\tv := versions[targetIndex-1].String()\n\t\treturn v, v\n\tdefault:\n\t\tsort.Sort(version.Collection(versions))\n\t\treturn versions[targetIndex-1].String(), versions[l-1].String()\n\t}\n}\n","structuredPatch":[{"oldStart":294,"oldLines":9,"newStart":294,"newLines":9,"lines":[" localUsers++"," } else {"," idpUsers++","- idpType := extractIdpType(idpID)","- embeddedIdpTypes[idpType]++"," }","+ idpType := extractIdpType(idpID)","+ embeddedIdpTypes[idpType]++"," }"," }"," }"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"264a6317-2fcb-4248-8455-bb1c2f548798"} +{"parentUuid":"be4da50f-f284-4ff4-a020-95533ab774ae","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01UyBPrZziAFbsBwvX9UqwzN","type":"message","role":"assistant","content":[{"type":"text","text":"Now fix `extractIdpType` to return the connectorID as-is when there's no dash (instead of hardcoding `\"oidc\"`):"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":501,"cache_read_input_tokens":112718,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":501},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhqE8DGiUpa19BLUPLuT","type":"assistant","uuid":"fe53da10-5303-4b7f-9535-86b5fa8b8f47","timestamp":"2026-03-04T09:50:29.327Z"} +{"parentUuid":"fe53da10-5303-4b7f-9535-86b5fa8b8f47","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01UyBPrZziAFbsBwvX9UqwzN","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_013rftmUCFL3iRAWcNjFmSTa","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go","old_string":"func extractIdpType(connectorID string) string {\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn \"oidc\"\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}","new_string":"func extractIdpType(connectorID string) string {\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn strings.ToLower(connectorID)\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":501,"cache_read_input_tokens":112718,"output_tokens":306,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":501,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqE8DGiUpa19BLUPLuT","type":"assistant","uuid":"acd857eb-c4fd-48d7-b7c0-8ac057fd0ad9","timestamp":"2026-03-04T09:50:31.085Z"} +{"parentUuid":"acd857eb-c4fd-48d7-b7c0-8ac057fd0ad9","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_013rftmUCFL3iRAWcNjFmSTa","toolUseID":"toolu_013rftmUCFL3iRAWcNjFmSTa","timestamp":"2026-03-04T09:51:18.782Z","uuid":"07978c5d-4d24-4efb-878f-11933f379e61"} +{"parentUuid":"acd857eb-c4fd-48d7-b7c0-8ac057fd0ad9","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_013rftmUCFL3iRAWcNjFmSTa","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go has been updated successfully."}]},"uuid":"859eb881-9c3d-400f-a35b-57f269748766","timestamp":"2026-03-04T09:51:18.780Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go","oldString":"// Package metrics gather anonymous information about the usage of NetBird management\npackage metrics\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\tnbversion \"github.com/netbirdio/netbird/version\"\n)\n\nconst (\n\t// PayloadEvent identifies an event type\n\tPayloadEvent = \"self-hosted stats\"\n\t// payloadEndpoint metrics defaultEndpoint to send anonymous data\n\tpayloadEndpoint = \"https://metrics.netbird.io\"\n\t// defaultPushInterval default interval to push metrics\n\tdefaultPushInterval = 12 * time.Hour\n\t// requestTimeout http request timeout\n\trequestTimeout = 45 * time.Second\n\tEmbeddedType = \"embedded\"\n)\n\ntype getTokenResponse struct {\n\tPublicAPIToken string `json:\"public_api_token\"`\n}\n\ntype pushPayload struct {\n\tAPIKey string `json:\"api_key\"`\n\tDistinctID string `json:\"distinct_id\"`\n\tEvent string `json:\"event\"`\n\tProperties properties `json:\"properties\"`\n\tTimestamp time.Time `json:\"timestamp\"`\n}\n\n// properties metrics to push\ntype properties map[string]interface{}\n\n// DataSource metric data source\ntype DataSource interface {\n\tGetAllAccounts(ctx context.Context) []*types.Account\n\tGetStoreEngine() types.Engine\n\tGetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)\n}\n\n// ConnManager peer connection manager that holds state for current active connections\ntype ConnManager interface {\n\tGetAllConnectedPeers() map[string]struct{}\n}\n\n// Worker metrics collector and pusher\ntype Worker struct {\n\tctx context.Context\n\tid string\n\tidpManager string\n\tdataSource DataSource\n\tconnManager ConnManager\n\tstartupTime time.Time\n\tlastRun time.Time\n}\n\n// NewWorker returns a metrics worker\nfunc NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {\n\tcurrentTime := time.Now()\n\treturn &Worker{\n\t\tctx: ctx,\n\t\tid: id,\n\t\tidpManager: idpManager,\n\t\tdataSource: dataSource,\n\t\tconnManager: connManager,\n\t\tstartupTime: currentTime,\n\t\tlastRun: currentTime,\n\t}\n}\n\n// Run runs the metrics worker\nfunc (w *Worker) Run(ctx context.Context) {\n\tinterval := getMetricsInterval(ctx)\n\n\tpushTicker := time.NewTicker(interval)\n\tfor {\n\t\tselect {\n\t\tcase <-w.ctx.Done():\n\t\t\treturn\n\t\tcase <-pushTicker.C:\n\t\t\terr := w.sendMetrics(ctx)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithContext(ctx).Error(err)\n\t\t\t}\n\t\t\tw.lastRun = time.Now()\n\t\t}\n\t}\n}\n\nfunc getMetricsInterval(ctx context.Context) time.Duration {\n\tinterval := defaultPushInterval\n\tif os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") != \"\" {\n\t\tnewInterval, err := time.ParseDuration(os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") + \"s\")\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"unable to parse NETBIRD_METRICS_INTERVAL_IN_SECONDS, using default interval %v. Error: %v\", defaultPushInterval, err)\n\t\t} else {\n\t\t\tlog.WithContext(ctx).Infof(\"using NETBIRD_METRICS_INTERVAL_IN_SECONDS %s\", newInterval)\n\t\t\tinterval = newInterval\n\t\t}\n\t}\n\treturn interval\n}\n\nfunc (w *Worker) sendMetrics(ctx context.Context) error {\n\tapiKey, err := getAPIKey(w.ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpayload := w.generatePayload(ctx, apiKey)\n\n\tpayloadString, err := buildMetricsPayload(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thttpClient := http.Client{}\n\n\texportJobReq, cancelCTX, err := createPostRequest(w.ctx, payloadEndpoint+\"/capture/\", payloadString)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create metrics post request %v\", err)\n\t}\n\tdefer cancelCTX()\n\n\tjobResp, err := httpClient.Do(exportJobReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to push metrics %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = jobResp.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing update metrics response body: %v\", err)\n\t\t}\n\t}()\n\n\tif jobResp.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"unable to push anonymous metrics, got statusCode %d\", jobResp.StatusCode)\n\t}\n\n\tlog.WithContext(ctx).Infof(\"sent anonymous metrics, next push will happen in %s. \"+\n\t\t\"You can disable these metrics by running with flag --disable-anonymous-metrics,\"+\n\t\t\" see more information at https://docs.netbird.io/about-netbird/faq#why-and-what-are-the-anonymous-usage-metrics\", getMetricsInterval(ctx))\n\n\treturn nil\n}\n\nfunc (w *Worker) generatePayload(ctx context.Context, apiKey string) pushPayload {\n\tproperties := w.generateProperties(ctx)\n\n\treturn pushPayload{\n\t\tAPIKey: apiKey,\n\t\tDistinctID: w.id,\n\t\tEvent: PayloadEvent,\n\t\tProperties: properties,\n\t\tTimestamp: time.Now(),\n\t}\n}\n\nfunc (w *Worker) generateProperties(ctx context.Context) properties {\n\tvar (\n\t\tuptime float64\n\t\taccounts int\n\t\texpirationEnabled int\n\t\tusers int\n\t\tserviceUsers int\n\t\tpats int\n\t\tpeers int\n\t\tpeersSSHEnabled int\n\t\tsetupKeysUsage int\n\t\tephemeralPeersSKs int\n\t\tephemeralPeersSKUsage int\n\t\tactivePeersLastDay int\n\t\tactiveUserPeersLastDay int\n\t\tosPeers map[string]int\n\t\tactiveUsersLastDay map[string]struct{}\n\t\tuserPeers int\n\t\trules int\n\t\trulesProtocol map[string]int\n\t\trulesDirection map[string]int\n\t\trulesWithSrcPostureChecks int\n\t\tpostureChecks int\n\t\tgroups int\n\t\troutes int\n\t\troutesWithRGGroups int\n\t\tnetworks int\n\t\tnetworkResources int\n\t\tnetworkRouters int\n\t\tnetworkRoutersWithPG int\n\t\tnameservers int\n\t\tuiClient int\n\t\tversion string\n\t\tpeerActiveVersions []string\n\t\tosUIClients map[string]int\n\t\trosenpassEnabled int\n\t\tlocalUsers int\n\t\tidpUsers int\n\t\tembeddedIdpTypes map[string]int\n\t\tservices int\n\t\tservicesEnabled int\n\t\tservicesTargets int\n\t\tservicesStatusActive int\n\t\tservicesStatusPending int\n\t\tservicesStatusError int\n\t\tservicesTargetType map[string]int\n\t\tservicesAuthPassword int\n\t\tservicesAuthPin int\n\t\tservicesAuthOIDC int\n\t)\n\tstart := time.Now()\n\tmetricsProperties := make(properties)\n\tosPeers = make(map[string]int)\n\tosUIClients = make(map[string]int)\n\trulesProtocol = make(map[string]int)\n\trulesDirection = make(map[string]int)\n\tactiveUsersLastDay = make(map[string]struct{})\n\tembeddedIdpTypes = make(map[string]int)\n\tservicesTargetType = make(map[string]int)\n\tuptime = time.Since(w.startupTime).Seconds()\n\tconnections := w.connManager.GetAllConnectedPeers()\n\tversion = nbversion.NetbirdVersion()\n\n\tcustomDomains, customDomainsValidated, _ := w.dataSource.GetCustomDomainsCounts(ctx)\n\n\tfor _, account := range w.dataSource.GetAllAccounts(ctx) {\n\t\taccounts++\n\n\t\tif account.Settings.PeerLoginExpirationEnabled {\n\t\t\texpirationEnabled++\n\t\t}\n\n\t\tgroups += len(account.Groups)\n\t\tnetworks += len(account.Networks)\n\t\tnetworkResources += len(account.NetworkResources)\n\n\t\tnetworkRouters += len(account.NetworkRouters)\n\t\tfor _, router := range account.NetworkRouters {\n\t\t\tif len(router.PeerGroups) > 0 {\n\t\t\t\tnetworkRoutersWithPG++\n\t\t\t}\n\t\t}\n\n\t\troutes += len(account.Routes)\n\t\tfor _, route := range account.Routes {\n\t\t\tif len(route.PeerGroups) > 0 {\n\t\t\t\troutesWithRGGroups++\n\t\t\t}\n\t\t}\n\t\tnameservers += len(account.NameServerGroups)\n\n\t\tfor _, policy := range account.Policies {\n\t\t\tfor _, rule := range policy.Rules {\n\t\t\t\trules++\n\t\t\t\trulesProtocol[string(rule.Protocol)]++\n\t\t\t\tif rule.Bidirectional {\n\t\t\t\t\trulesDirection[\"bidirectional\"]++\n\t\t\t\t} else {\n\t\t\t\t\trulesDirection[\"oneway\"]++\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(policy.SourcePostureChecks) > 0 {\n\t\t\t\trulesWithSrcPostureChecks++\n\t\t\t}\n\t\t}\n\n\t\tpostureChecks += len(account.PostureChecks)\n\n\t\tfor _, user := range account.Users {\n\t\t\tif user.IsServiceUser {\n\t\t\t\tserviceUsers++\n\t\t\t} else {\n\t\t\t\tusers++\n\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t}\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpats += len(user.PATs)\n\t\t}\n\n\t\tfor _, key := range account.SetupKeys {\n\t\t\tsetupKeysUsage += key.UsedTimes\n\t\t\tif key.Ephemeral {\n\t\t\t\tephemeralPeersSKs++\n\t\t\t\tephemeralPeersSKUsage += key.UsedTimes\n\t\t\t}\n\t\t}\n\n\t\tfor _, peer := range account.Peers {\n\t\t\tpeers++\n\n\t\t\tif peer.SSHEnabled || peer.Meta.Flags.ServerSSHAllowed {\n\t\t\t\tpeersSSHEnabled++\n\t\t\t}\n\n\t\t\tif peer.Meta.Flags.RosenpassEnabled {\n\t\t\t\trosenpassEnabled++\n\t\t\t}\n\n\t\t\tif peer.UserID != \"\" {\n\t\t\t\tuserPeers++\n\t\t\t}\n\n\t\t\tosKey := strings.ToLower(fmt.Sprintf(\"peer_os_%s\", peer.Meta.GoOS))\n\t\t\tosCount := osPeers[osKey]\n\t\t\tosPeers[osKey] = osCount + 1\n\n\t\t\tif peer.Meta.UIVersion != \"\" {\n\t\t\t\tuiClient++\n\t\t\t\tuiOSKey := strings.ToLower(fmt.Sprintf(\"ui_client_os_%s\", peer.Meta.GoOS))\n\t\t\t\tosUICount := osUIClients[uiOSKey]\n\t\t\t\tosUIClients[uiOSKey] = osUICount + 1\n\t\t\t}\n\n\t\t\t_, connected := connections[peer.ID]\n\t\t\tif connected || peer.Status.LastSeen.After(w.lastRun) {\n\t\t\t\tactivePeersLastDay++\n\t\t\t\tif peer.UserID != \"\" {\n\t\t\t\t\tactiveUserPeersLastDay++\n\t\t\t\t\tactiveUsersLastDay[peer.UserID] = struct{}{}\n\t\t\t\t}\n\t\t\t\tosActiveKey := osKey + \"_active\"\n\t\t\t\tosActiveCount := osPeers[osActiveKey]\n\t\t\t\tosPeers[osActiveKey] = osActiveCount + 1\n\t\t\t\tpeerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)\n\t\t\t}\n\t\t}\n\n\t\tfor _, service := range account.Services {\n\t\t\tservices++\n\t\t\tif service.Enabled {\n\t\t\t\tservicesEnabled++\n\t\t\t}\n\t\t\tservicesTargets += len(service.Targets)\n\n\t\t\tswitch rpservice.Status(service.Meta.Status) {\n\t\t\tcase rpservice.StatusActive:\n\t\t\t\tservicesStatusActive++\n\t\t\tcase rpservice.StatusPending:\n\t\t\t\tservicesStatusPending++\n\t\t\tcase rpservice.StatusError, rpservice.StatusCertificateFailed, rpservice.StatusTunnelNotCreated:\n\t\t\t\tservicesStatusError++\n\t\t\t}\n\n\t\t\tfor _, target := range service.Targets {\n\t\t\t\tservicesTargetType[target.TargetType]++\n\t\t\t}\n\n\t\t\tif service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled {\n\t\t\t\tservicesAuthPassword++\n\t\t\t}\n\t\t\tif service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled {\n\t\t\t\tservicesAuthPin++\n\t\t\t}\n\t\t\tif service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled {\n\t\t\t\tservicesAuthOIDC++\n\t\t\t}\n\t\t}\n\t}\n\n\tminActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)\n\tmetricsProperties[\"uptime\"] = uptime\n\tmetricsProperties[\"accounts\"] = accounts\n\tmetricsProperties[\"users\"] = users\n\tmetricsProperties[\"service_users\"] = serviceUsers\n\tmetricsProperties[\"pats\"] = pats\n\tmetricsProperties[\"peers\"] = peers\n\tmetricsProperties[\"peers_ssh_enabled\"] = peersSSHEnabled\n\tmetricsProperties[\"peers_login_expiration_enabled\"] = expirationEnabled\n\tmetricsProperties[\"setup_keys_usage\"] = setupKeysUsage\n\tmetricsProperties[\"ephemeral_peers_setup_keys\"] = ephemeralPeersSKs\n\tmetricsProperties[\"ephemeral_peers_setup_keys_usage\"] = ephemeralPeersSKUsage\n\tmetricsProperties[\"active_peers_last_day\"] = activePeersLastDay\n\tmetricsProperties[\"active_user_peers_last_day\"] = activeUserPeersLastDay\n\tmetricsProperties[\"active_users_last_day\"] = len(activeUsersLastDay)\n\tmetricsProperties[\"user_peers\"] = userPeers\n\tmetricsProperties[\"rules\"] = rules\n\tmetricsProperties[\"rules_with_src_posture_checks\"] = rulesWithSrcPostureChecks\n\tmetricsProperties[\"posture_checks\"] = postureChecks\n\tmetricsProperties[\"groups\"] = groups\n\tmetricsProperties[\"networks\"] = networks\n\tmetricsProperties[\"network_resources\"] = networkResources\n\tmetricsProperties[\"network_routers\"] = networkRouters\n\tmetricsProperties[\"network_routers_with_groups\"] = networkRoutersWithPG\n\tmetricsProperties[\"routes\"] = routes\n\tmetricsProperties[\"routes_with_routing_groups\"] = routesWithRGGroups\n\tmetricsProperties[\"nameservers\"] = nameservers\n\tmetricsProperties[\"version\"] = version\n\tmetricsProperties[\"min_active_peer_version\"] = minActivePeerVersion\n\tmetricsProperties[\"max_active_peer_version\"] = maxActivePeerVersion\n\tmetricsProperties[\"ui_clients\"] = uiClient\n\tmetricsProperties[\"idp_manager\"] = w.idpManager\n\tmetricsProperties[\"store_engine\"] = w.dataSource.GetStoreEngine()\n\tmetricsProperties[\"rosenpass_enabled\"] = rosenpassEnabled\n\tmetricsProperties[\"local_users_count\"] = localUsers\n\tmetricsProperties[\"idp_users_count\"] = idpUsers\n\tmetricsProperties[\"embedded_idp_count\"] = len(embeddedIdpTypes)\n\n\tmetricsProperties[\"services\"] = services\n\tmetricsProperties[\"services_enabled\"] = servicesEnabled\n\tmetricsProperties[\"services_targets\"] = servicesTargets\n\tmetricsProperties[\"services_status_active\"] = servicesStatusActive\n\tmetricsProperties[\"services_status_pending\"] = servicesStatusPending\n\tmetricsProperties[\"services_status_error\"] = servicesStatusError\n\tmetricsProperties[\"services_auth_password\"] = servicesAuthPassword\n\tmetricsProperties[\"services_auth_pin\"] = servicesAuthPin\n\tmetricsProperties[\"services_auth_oidc\"] = servicesAuthOIDC\n\tmetricsProperties[\"custom_domains\"] = customDomains\n\tmetricsProperties[\"custom_domains_validated\"] = customDomainsValidated\n\n\tfor targetType, count := range servicesTargetType {\n\t\tmetricsProperties[\"services_target_type_\"+targetType] = count\n\t}\n\n\tfor idpType, count := range embeddedIdpTypes {\n\t\tmetricsProperties[\"embedded_idp_users_\"+idpType] = count\n\t}\n\n\tfor protocol, count := range rulesProtocol {\n\t\tmetricsProperties[\"rules_protocol_\"+protocol] = count\n\t}\n\n\tfor direction, count := range rulesDirection {\n\t\tmetricsProperties[\"rules_direction_\"+direction] = count\n\t}\n\n\tfor os, count := range osPeers {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tfor os, count := range osUIClients {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tmetricsProperties[\"metric_generation_time\"] = time.Since(start).Milliseconds()\n\n\treturn metricsProperties\n}\n\nfunc getAPIKey(ctx context.Context) (string, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\tdefer cancel()\n\n\thttpClient := http.Client{}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+\"/GetToken\", nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create request for metrics public api token %v\", err)\n\t}\n\tresponse, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to request metrics public api token %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing metrics token response body: %v\", err)\n\t\t}\n\t}()\n\n\tif response.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"unable to retrieve metrics token, statusCode %d\", response.StatusCode)\n\t}\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't get metrics token response; %v\", err)\n\t}\n\n\tvar tokenResponse getTokenResponse\n\n\terr = json.Unmarshal(body, &tokenResponse)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't parse metrics public api token; %v\", err)\n\t}\n\n\treturn tokenResponse.PublicAPIToken, nil\n}\n\nfunc buildMetricsPayload(payload pushPayload) (string, error) {\n\tstr, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to marshal metrics payload, got err: %v\", err)\n\t}\n\treturn string(str), nil\n}\n\nfunc createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, context.CancelFunc, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\treqURL := endpoint\n\n\tpayload := strings.NewReader(payloadStr)\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", reqURL, payload)\n\tif err != nil {\n\t\tcancel()\n\t\treturn nil, nil, err\n\t}\n\treq.Header.Add(\"content-type\", \"application/json\")\n\n\treturn req, cancel, nil\n}\n\n// extractIdpType extracts the IdP type from a Dex connector ID.\n// Connector IDs are formatted as \" - \" (e.g., \"okta-abc123\", \"zitadel-xyz\").\n// Returns the type prefix, or \"oidc\" if no known prefix is found.\nfunc extractIdpType(connectorID string) string {\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn \"oidc\"\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}\n\nfunc getMinMaxVersion(inputList []string) (string, string) {\n\tversions := make([]*version.Version, 0)\n\n\tfor _, raw := range inputList {\n\t\tif raw != \"\" && nbversion.SemverRegexp.MatchString(raw) {\n\t\t\tv, err := version.NewVersion(raw)\n\t\t\tif err == nil {\n\t\t\t\tversions = append(versions, v)\n\t\t\t}\n\t\t}\n\t}\n\n\ttargetIndex := 1\n\tl := len(versions)\n\n\tswitch l {\n\tcase 0:\n\t\treturn \"\", \"\"\n\tcase targetIndex:\n\t\tv := versions[targetIndex-1].String()\n\t\treturn v, v\n\tdefault:\n\t\tsort.Sort(version.Collection(versions))\n\t\treturn versions[targetIndex-1].String(), versions[l-1].String()\n\t}\n}","newString":"// Package metrics gather anonymous information about the usage of NetBird management\npackage metrics\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\tnbversion \"github.com/netbirdio/netbird/version\"\n)\n\nconst (\n\t// PayloadEvent identifies an event type\n\tPayloadEvent = \"self-hosted stats\"\n\t// payloadEndpoint metrics defaultEndpoint to send anonymous data\n\tpayloadEndpoint = \"https://metrics.netbird.io\"\n\t// defaultPushInterval default interval to push metrics\n\tdefaultPushInterval = 12 * time.Hour\n\t// requestTimeout http request timeout\n\trequestTimeout = 45 * time.Second\n\tEmbeddedType = \"embedded\"\n)\n\ntype getTokenResponse struct {\n\tPublicAPIToken string `json:\"public_api_token\"`\n}\n\ntype pushPayload struct {\n\tAPIKey string `json:\"api_key\"`\n\tDistinctID string `json:\"distinct_id\"`\n\tEvent string `json:\"event\"`\n\tProperties properties `json:\"properties\"`\n\tTimestamp time.Time `json:\"timestamp\"`\n}\n\n// properties metrics to push\ntype properties map[string]interface{}\n\n// DataSource metric data source\ntype DataSource interface {\n\tGetAllAccounts(ctx context.Context) []*types.Account\n\tGetStoreEngine() types.Engine\n\tGetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)\n}\n\n// ConnManager peer connection manager that holds state for current active connections\ntype ConnManager interface {\n\tGetAllConnectedPeers() map[string]struct{}\n}\n\n// Worker metrics collector and pusher\ntype Worker struct {\n\tctx context.Context\n\tid string\n\tidpManager string\n\tdataSource DataSource\n\tconnManager ConnManager\n\tstartupTime time.Time\n\tlastRun time.Time\n}\n\n// NewWorker returns a metrics worker\nfunc NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {\n\tcurrentTime := time.Now()\n\treturn &Worker{\n\t\tctx: ctx,\n\t\tid: id,\n\t\tidpManager: idpManager,\n\t\tdataSource: dataSource,\n\t\tconnManager: connManager,\n\t\tstartupTime: currentTime,\n\t\tlastRun: currentTime,\n\t}\n}\n\n// Run runs the metrics worker\nfunc (w *Worker) Run(ctx context.Context) {\n\tinterval := getMetricsInterval(ctx)\n\n\tpushTicker := time.NewTicker(interval)\n\tfor {\n\t\tselect {\n\t\tcase <-w.ctx.Done():\n\t\t\treturn\n\t\tcase <-pushTicker.C:\n\t\t\terr := w.sendMetrics(ctx)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithContext(ctx).Error(err)\n\t\t\t}\n\t\t\tw.lastRun = time.Now()\n\t\t}\n\t}\n}\n\nfunc getMetricsInterval(ctx context.Context) time.Duration {\n\tinterval := defaultPushInterval\n\tif os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") != \"\" {\n\t\tnewInterval, err := time.ParseDuration(os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") + \"s\")\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"unable to parse NETBIRD_METRICS_INTERVAL_IN_SECONDS, using default interval %v. Error: %v\", defaultPushInterval, err)\n\t\t} else {\n\t\t\tlog.WithContext(ctx).Infof(\"using NETBIRD_METRICS_INTERVAL_IN_SECONDS %s\", newInterval)\n\t\t\tinterval = newInterval\n\t\t}\n\t}\n\treturn interval\n}\n\nfunc (w *Worker) sendMetrics(ctx context.Context) error {\n\tapiKey, err := getAPIKey(w.ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpayload := w.generatePayload(ctx, apiKey)\n\n\tpayloadString, err := buildMetricsPayload(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thttpClient := http.Client{}\n\n\texportJobReq, cancelCTX, err := createPostRequest(w.ctx, payloadEndpoint+\"/capture/\", payloadString)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create metrics post request %v\", err)\n\t}\n\tdefer cancelCTX()\n\n\tjobResp, err := httpClient.Do(exportJobReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to push metrics %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = jobResp.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing update metrics response body: %v\", err)\n\t\t}\n\t}()\n\n\tif jobResp.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"unable to push anonymous metrics, got statusCode %d\", jobResp.StatusCode)\n\t}\n\n\tlog.WithContext(ctx).Infof(\"sent anonymous metrics, next push will happen in %s. \"+\n\t\t\"You can disable these metrics by running with flag --disable-anonymous-metrics,\"+\n\t\t\" see more information at https://docs.netbird.io/about-netbird/faq#why-and-what-are-the-anonymous-usage-metrics\", getMetricsInterval(ctx))\n\n\treturn nil\n}\n\nfunc (w *Worker) generatePayload(ctx context.Context, apiKey string) pushPayload {\n\tproperties := w.generateProperties(ctx)\n\n\treturn pushPayload{\n\t\tAPIKey: apiKey,\n\t\tDistinctID: w.id,\n\t\tEvent: PayloadEvent,\n\t\tProperties: properties,\n\t\tTimestamp: time.Now(),\n\t}\n}\n\nfunc (w *Worker) generateProperties(ctx context.Context) properties {\n\tvar (\n\t\tuptime float64\n\t\taccounts int\n\t\texpirationEnabled int\n\t\tusers int\n\t\tserviceUsers int\n\t\tpats int\n\t\tpeers int\n\t\tpeersSSHEnabled int\n\t\tsetupKeysUsage int\n\t\tephemeralPeersSKs int\n\t\tephemeralPeersSKUsage int\n\t\tactivePeersLastDay int\n\t\tactiveUserPeersLastDay int\n\t\tosPeers map[string]int\n\t\tactiveUsersLastDay map[string]struct{}\n\t\tuserPeers int\n\t\trules int\n\t\trulesProtocol map[string]int\n\t\trulesDirection map[string]int\n\t\trulesWithSrcPostureChecks int\n\t\tpostureChecks int\n\t\tgroups int\n\t\troutes int\n\t\troutesWithRGGroups int\n\t\tnetworks int\n\t\tnetworkResources int\n\t\tnetworkRouters int\n\t\tnetworkRoutersWithPG int\n\t\tnameservers int\n\t\tuiClient int\n\t\tversion string\n\t\tpeerActiveVersions []string\n\t\tosUIClients map[string]int\n\t\trosenpassEnabled int\n\t\tlocalUsers int\n\t\tidpUsers int\n\t\tembeddedIdpTypes map[string]int\n\t\tservices int\n\t\tservicesEnabled int\n\t\tservicesTargets int\n\t\tservicesStatusActive int\n\t\tservicesStatusPending int\n\t\tservicesStatusError int\n\t\tservicesTargetType map[string]int\n\t\tservicesAuthPassword int\n\t\tservicesAuthPin int\n\t\tservicesAuthOIDC int\n\t)\n\tstart := time.Now()\n\tmetricsProperties := make(properties)\n\tosPeers = make(map[string]int)\n\tosUIClients = make(map[string]int)\n\trulesProtocol = make(map[string]int)\n\trulesDirection = make(map[string]int)\n\tactiveUsersLastDay = make(map[string]struct{})\n\tembeddedIdpTypes = make(map[string]int)\n\tservicesTargetType = make(map[string]int)\n\tuptime = time.Since(w.startupTime).Seconds()\n\tconnections := w.connManager.GetAllConnectedPeers()\n\tversion = nbversion.NetbirdVersion()\n\n\tcustomDomains, customDomainsValidated, _ := w.dataSource.GetCustomDomainsCounts(ctx)\n\n\tfor _, account := range w.dataSource.GetAllAccounts(ctx) {\n\t\taccounts++\n\n\t\tif account.Settings.PeerLoginExpirationEnabled {\n\t\t\texpirationEnabled++\n\t\t}\n\n\t\tgroups += len(account.Groups)\n\t\tnetworks += len(account.Networks)\n\t\tnetworkResources += len(account.NetworkResources)\n\n\t\tnetworkRouters += len(account.NetworkRouters)\n\t\tfor _, router := range account.NetworkRouters {\n\t\t\tif len(router.PeerGroups) > 0 {\n\t\t\t\tnetworkRoutersWithPG++\n\t\t\t}\n\t\t}\n\n\t\troutes += len(account.Routes)\n\t\tfor _, route := range account.Routes {\n\t\t\tif len(route.PeerGroups) > 0 {\n\t\t\t\troutesWithRGGroups++\n\t\t\t}\n\t\t}\n\t\tnameservers += len(account.NameServerGroups)\n\n\t\tfor _, policy := range account.Policies {\n\t\t\tfor _, rule := range policy.Rules {\n\t\t\t\trules++\n\t\t\t\trulesProtocol[string(rule.Protocol)]++\n\t\t\t\tif rule.Bidirectional {\n\t\t\t\t\trulesDirection[\"bidirectional\"]++\n\t\t\t\t} else {\n\t\t\t\t\trulesDirection[\"oneway\"]++\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(policy.SourcePostureChecks) > 0 {\n\t\t\t\trulesWithSrcPostureChecks++\n\t\t\t}\n\t\t}\n\n\t\tpostureChecks += len(account.PostureChecks)\n\n\t\tfor _, user := range account.Users {\n\t\t\tif user.IsServiceUser {\n\t\t\t\tserviceUsers++\n\t\t\t} else {\n\t\t\t\tusers++\n\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t}\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpats += len(user.PATs)\n\t\t}\n\n\t\tfor _, key := range account.SetupKeys {\n\t\t\tsetupKeysUsage += key.UsedTimes\n\t\t\tif key.Ephemeral {\n\t\t\t\tephemeralPeersSKs++\n\t\t\t\tephemeralPeersSKUsage += key.UsedTimes\n\t\t\t}\n\t\t}\n\n\t\tfor _, peer := range account.Peers {\n\t\t\tpeers++\n\n\t\t\tif peer.SSHEnabled || peer.Meta.Flags.ServerSSHAllowed {\n\t\t\t\tpeersSSHEnabled++\n\t\t\t}\n\n\t\t\tif peer.Meta.Flags.RosenpassEnabled {\n\t\t\t\trosenpassEnabled++\n\t\t\t}\n\n\t\t\tif peer.UserID != \"\" {\n\t\t\t\tuserPeers++\n\t\t\t}\n\n\t\t\tosKey := strings.ToLower(fmt.Sprintf(\"peer_os_%s\", peer.Meta.GoOS))\n\t\t\tosCount := osPeers[osKey]\n\t\t\tosPeers[osKey] = osCount + 1\n\n\t\t\tif peer.Meta.UIVersion != \"\" {\n\t\t\t\tuiClient++\n\t\t\t\tuiOSKey := strings.ToLower(fmt.Sprintf(\"ui_client_os_%s\", peer.Meta.GoOS))\n\t\t\t\tosUICount := osUIClients[uiOSKey]\n\t\t\t\tosUIClients[uiOSKey] = osUICount + 1\n\t\t\t}\n\n\t\t\t_, connected := connections[peer.ID]\n\t\t\tif connected || peer.Status.LastSeen.After(w.lastRun) {\n\t\t\t\tactivePeersLastDay++\n\t\t\t\tif peer.UserID != \"\" {\n\t\t\t\t\tactiveUserPeersLastDay++\n\t\t\t\t\tactiveUsersLastDay[peer.UserID] = struct{}{}\n\t\t\t\t}\n\t\t\t\tosActiveKey := osKey + \"_active\"\n\t\t\t\tosActiveCount := osPeers[osActiveKey]\n\t\t\t\tosPeers[osActiveKey] = osActiveCount + 1\n\t\t\t\tpeerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)\n\t\t\t}\n\t\t}\n\n\t\tfor _, service := range account.Services {\n\t\t\tservices++\n\t\t\tif service.Enabled {\n\t\t\t\tservicesEnabled++\n\t\t\t}\n\t\t\tservicesTargets += len(service.Targets)\n\n\t\t\tswitch rpservice.Status(service.Meta.Status) {\n\t\t\tcase rpservice.StatusActive:\n\t\t\t\tservicesStatusActive++\n\t\t\tcase rpservice.StatusPending:\n\t\t\t\tservicesStatusPending++\n\t\t\tcase rpservice.StatusError, rpservice.StatusCertificateFailed, rpservice.StatusTunnelNotCreated:\n\t\t\t\tservicesStatusError++\n\t\t\t}\n\n\t\t\tfor _, target := range service.Targets {\n\t\t\t\tservicesTargetType[target.TargetType]++\n\t\t\t}\n\n\t\t\tif service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled {\n\t\t\t\tservicesAuthPassword++\n\t\t\t}\n\t\t\tif service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled {\n\t\t\t\tservicesAuthPin++\n\t\t\t}\n\t\t\tif service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled {\n\t\t\t\tservicesAuthOIDC++\n\t\t\t}\n\t\t}\n\t}\n\n\tminActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)\n\tmetricsProperties[\"uptime\"] = uptime\n\tmetricsProperties[\"accounts\"] = accounts\n\tmetricsProperties[\"users\"] = users\n\tmetricsProperties[\"service_users\"] = serviceUsers\n\tmetricsProperties[\"pats\"] = pats\n\tmetricsProperties[\"peers\"] = peers\n\tmetricsProperties[\"peers_ssh_enabled\"] = peersSSHEnabled\n\tmetricsProperties[\"peers_login_expiration_enabled\"] = expirationEnabled\n\tmetricsProperties[\"setup_keys_usage\"] = setupKeysUsage\n\tmetricsProperties[\"ephemeral_peers_setup_keys\"] = ephemeralPeersSKs\n\tmetricsProperties[\"ephemeral_peers_setup_keys_usage\"] = ephemeralPeersSKUsage\n\tmetricsProperties[\"active_peers_last_day\"] = activePeersLastDay\n\tmetricsProperties[\"active_user_peers_last_day\"] = activeUserPeersLastDay\n\tmetricsProperties[\"active_users_last_day\"] = len(activeUsersLastDay)\n\tmetricsProperties[\"user_peers\"] = userPeers\n\tmetricsProperties[\"rules\"] = rules\n\tmetricsProperties[\"rules_with_src_posture_checks\"] = rulesWithSrcPostureChecks\n\tmetricsProperties[\"posture_checks\"] = postureChecks\n\tmetricsProperties[\"groups\"] = groups\n\tmetricsProperties[\"networks\"] = networks\n\tmetricsProperties[\"network_resources\"] = networkResources\n\tmetricsProperties[\"network_routers\"] = networkRouters\n\tmetricsProperties[\"network_routers_with_groups\"] = networkRoutersWithPG\n\tmetricsProperties[\"routes\"] = routes\n\tmetricsProperties[\"routes_with_routing_groups\"] = routesWithRGGroups\n\tmetricsProperties[\"nameservers\"] = nameservers\n\tmetricsProperties[\"version\"] = version\n\tmetricsProperties[\"min_active_peer_version\"] = minActivePeerVersion\n\tmetricsProperties[\"max_active_peer_version\"] = maxActivePeerVersion\n\tmetricsProperties[\"ui_clients\"] = uiClient\n\tmetricsProperties[\"idp_manager\"] = w.idpManager\n\tmetricsProperties[\"store_engine\"] = w.dataSource.GetStoreEngine()\n\tmetricsProperties[\"rosenpass_enabled\"] = rosenpassEnabled\n\tmetricsProperties[\"local_users_count\"] = localUsers\n\tmetricsProperties[\"idp_users_count\"] = idpUsers\n\tmetricsProperties[\"embedded_idp_count\"] = len(embeddedIdpTypes)\n\n\tmetricsProperties[\"services\"] = services\n\tmetricsProperties[\"services_enabled\"] = servicesEnabled\n\tmetricsProperties[\"services_targets\"] = servicesTargets\n\tmetricsProperties[\"services_status_active\"] = servicesStatusActive\n\tmetricsProperties[\"services_status_pending\"] = servicesStatusPending\n\tmetricsProperties[\"services_status_error\"] = servicesStatusError\n\tmetricsProperties[\"services_auth_password\"] = servicesAuthPassword\n\tmetricsProperties[\"services_auth_pin\"] = servicesAuthPin\n\tmetricsProperties[\"services_auth_oidc\"] = servicesAuthOIDC\n\tmetricsProperties[\"custom_domains\"] = customDomains\n\tmetricsProperties[\"custom_domains_validated\"] = customDomainsValidated\n\n\tfor targetType, count := range servicesTargetType {\n\t\tmetricsProperties[\"services_target_type_\"+targetType] = count\n\t}\n\n\tfor idpType, count := range embeddedIdpTypes {\n\t\tmetricsProperties[\"embedded_idp_users_\"+idpType] = count\n\t}\n\n\tfor protocol, count := range rulesProtocol {\n\t\tmetricsProperties[\"rules_protocol_\"+protocol] = count\n\t}\n\n\tfor direction, count := range rulesDirection {\n\t\tmetricsProperties[\"rules_direction_\"+direction] = count\n\t}\n\n\tfor os, count := range osPeers {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tfor os, count := range osUIClients {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tmetricsProperties[\"metric_generation_time\"] = time.Since(start).Milliseconds()\n\n\treturn metricsProperties\n}\n\nfunc getAPIKey(ctx context.Context) (string, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\tdefer cancel()\n\n\thttpClient := http.Client{}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+\"/GetToken\", nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create request for metrics public api token %v\", err)\n\t}\n\tresponse, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to request metrics public api token %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing metrics token response body: %v\", err)\n\t\t}\n\t}()\n\n\tif response.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"unable to retrieve metrics token, statusCode %d\", response.StatusCode)\n\t}\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't get metrics token response; %v\", err)\n\t}\n\n\tvar tokenResponse getTokenResponse\n\n\terr = json.Unmarshal(body, &tokenResponse)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't parse metrics public api token; %v\", err)\n\t}\n\n\treturn tokenResponse.PublicAPIToken, nil\n}\n\nfunc buildMetricsPayload(payload pushPayload) (string, error) {\n\tstr, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to marshal metrics payload, got err: %v\", err)\n\t}\n\treturn string(str), nil\n}\n\nfunc createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, context.CancelFunc, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\treqURL := endpoint\n\n\tpayload := strings.NewReader(payloadStr)\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", reqURL, payload)\n\tif err != nil {\n\t\tcancel()\n\t\treturn nil, nil, err\n\t}\n\treq.Header.Add(\"content-type\", \"application/json\")\n\n\treturn req, cancel, nil\n}\n\n// extractIdpType extracts the IdP type from a Dex connector ID.\n// Connector IDs are formatted as \" - \" (e.g., \"okta-abc123\", \"zitadel-xyz\").\n// Returns the type prefix, or \"oidc\" if no known prefix is found.\nfunc extractIdpType(connectorID string) string {\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn strings.ToLower(connectorID)\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}\n\nfunc getMinMaxVersion(inputList []string) (string, string) {\n\tversions := make([]*version.Version, 0)\n\n\tfor _, raw := range inputList {\n\t\tif raw != \"\" && nbversion.SemverRegexp.MatchString(raw) {\n\t\t\tv, err := version.NewVersion(raw)\n\t\t\tif err == nil {\n\t\t\t\tversions = append(versions, v)\n\t\t\t}\n\t\t}\n\t}\n\n\ttargetIndex := 1\n\tl := len(versions)\n\n\tswitch l {\n\tcase 0:\n\t\treturn \"\", \"\"\n\tcase targetIndex:\n\t\tv := versions[targetIndex-1].String()\n\t\treturn v, v\n\tdefault:\n\t\tsort.Sort(version.Collection(versions))\n\t\treturn versions[targetIndex-1].String(), versions[l-1].String()\n\t}\n}","originalFile":"// Package metrics gather anonymous information about the usage of NetBird management\npackage metrics\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\tnbversion \"github.com/netbirdio/netbird/version\"\n)\n\nconst (\n\t// PayloadEvent identifies an event type\n\tPayloadEvent = \"self-hosted stats\"\n\t// payloadEndpoint metrics defaultEndpoint to send anonymous data\n\tpayloadEndpoint = \"https://metrics.netbird.io\"\n\t// defaultPushInterval default interval to push metrics\n\tdefaultPushInterval = 12 * time.Hour\n\t// requestTimeout http request timeout\n\trequestTimeout = 45 * time.Second\n\tEmbeddedType = \"embedded\"\n)\n\ntype getTokenResponse struct {\n\tPublicAPIToken string `json:\"public_api_token\"`\n}\n\ntype pushPayload struct {\n\tAPIKey string `json:\"api_key\"`\n\tDistinctID string `json:\"distinct_id\"`\n\tEvent string `json:\"event\"`\n\tProperties properties `json:\"properties\"`\n\tTimestamp time.Time `json:\"timestamp\"`\n}\n\n// properties metrics to push\ntype properties map[string]interface{}\n\n// DataSource metric data source\ntype DataSource interface {\n\tGetAllAccounts(ctx context.Context) []*types.Account\n\tGetStoreEngine() types.Engine\n\tGetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)\n}\n\n// ConnManager peer connection manager that holds state for current active connections\ntype ConnManager interface {\n\tGetAllConnectedPeers() map[string]struct{}\n}\n\n// Worker metrics collector and pusher\ntype Worker struct {\n\tctx context.Context\n\tid string\n\tidpManager string\n\tdataSource DataSource\n\tconnManager ConnManager\n\tstartupTime time.Time\n\tlastRun time.Time\n}\n\n// NewWorker returns a metrics worker\nfunc NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {\n\tcurrentTime := time.Now()\n\treturn &Worker{\n\t\tctx: ctx,\n\t\tid: id,\n\t\tidpManager: idpManager,\n\t\tdataSource: dataSource,\n\t\tconnManager: connManager,\n\t\tstartupTime: currentTime,\n\t\tlastRun: currentTime,\n\t}\n}\n\n// Run runs the metrics worker\nfunc (w *Worker) Run(ctx context.Context) {\n\tinterval := getMetricsInterval(ctx)\n\n\tpushTicker := time.NewTicker(interval)\n\tfor {\n\t\tselect {\n\t\tcase <-w.ctx.Done():\n\t\t\treturn\n\t\tcase <-pushTicker.C:\n\t\t\terr := w.sendMetrics(ctx)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithContext(ctx).Error(err)\n\t\t\t}\n\t\t\tw.lastRun = time.Now()\n\t\t}\n\t}\n}\n\nfunc getMetricsInterval(ctx context.Context) time.Duration {\n\tinterval := defaultPushInterval\n\tif os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") != \"\" {\n\t\tnewInterval, err := time.ParseDuration(os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") + \"s\")\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"unable to parse NETBIRD_METRICS_INTERVAL_IN_SECONDS, using default interval %v. Error: %v\", defaultPushInterval, err)\n\t\t} else {\n\t\t\tlog.WithContext(ctx).Infof(\"using NETBIRD_METRICS_INTERVAL_IN_SECONDS %s\", newInterval)\n\t\t\tinterval = newInterval\n\t\t}\n\t}\n\treturn interval\n}\n\nfunc (w *Worker) sendMetrics(ctx context.Context) error {\n\tapiKey, err := getAPIKey(w.ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpayload := w.generatePayload(ctx, apiKey)\n\n\tpayloadString, err := buildMetricsPayload(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thttpClient := http.Client{}\n\n\texportJobReq, cancelCTX, err := createPostRequest(w.ctx, payloadEndpoint+\"/capture/\", payloadString)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create metrics post request %v\", err)\n\t}\n\tdefer cancelCTX()\n\n\tjobResp, err := httpClient.Do(exportJobReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to push metrics %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = jobResp.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing update metrics response body: %v\", err)\n\t\t}\n\t}()\n\n\tif jobResp.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"unable to push anonymous metrics, got statusCode %d\", jobResp.StatusCode)\n\t}\n\n\tlog.WithContext(ctx).Infof(\"sent anonymous metrics, next push will happen in %s. \"+\n\t\t\"You can disable these metrics by running with flag --disable-anonymous-metrics,\"+\n\t\t\" see more information at https://docs.netbird.io/about-netbird/faq#why-and-what-are-the-anonymous-usage-metrics\", getMetricsInterval(ctx))\n\n\treturn nil\n}\n\nfunc (w *Worker) generatePayload(ctx context.Context, apiKey string) pushPayload {\n\tproperties := w.generateProperties(ctx)\n\n\treturn pushPayload{\n\t\tAPIKey: apiKey,\n\t\tDistinctID: w.id,\n\t\tEvent: PayloadEvent,\n\t\tProperties: properties,\n\t\tTimestamp: time.Now(),\n\t}\n}\n\nfunc (w *Worker) generateProperties(ctx context.Context) properties {\n\tvar (\n\t\tuptime float64\n\t\taccounts int\n\t\texpirationEnabled int\n\t\tusers int\n\t\tserviceUsers int\n\t\tpats int\n\t\tpeers int\n\t\tpeersSSHEnabled int\n\t\tsetupKeysUsage int\n\t\tephemeralPeersSKs int\n\t\tephemeralPeersSKUsage int\n\t\tactivePeersLastDay int\n\t\tactiveUserPeersLastDay int\n\t\tosPeers map[string]int\n\t\tactiveUsersLastDay map[string]struct{}\n\t\tuserPeers int\n\t\trules int\n\t\trulesProtocol map[string]int\n\t\trulesDirection map[string]int\n\t\trulesWithSrcPostureChecks int\n\t\tpostureChecks int\n\t\tgroups int\n\t\troutes int\n\t\troutesWithRGGroups int\n\t\tnetworks int\n\t\tnetworkResources int\n\t\tnetworkRouters int\n\t\tnetworkRoutersWithPG int\n\t\tnameservers int\n\t\tuiClient int\n\t\tversion string\n\t\tpeerActiveVersions []string\n\t\tosUIClients map[string]int\n\t\trosenpassEnabled int\n\t\tlocalUsers int\n\t\tidpUsers int\n\t\tembeddedIdpTypes map[string]int\n\t\tservices int\n\t\tservicesEnabled int\n\t\tservicesTargets int\n\t\tservicesStatusActive int\n\t\tservicesStatusPending int\n\t\tservicesStatusError int\n\t\tservicesTargetType map[string]int\n\t\tservicesAuthPassword int\n\t\tservicesAuthPin int\n\t\tservicesAuthOIDC int\n\t)\n\tstart := time.Now()\n\tmetricsProperties := make(properties)\n\tosPeers = make(map[string]int)\n\tosUIClients = make(map[string]int)\n\trulesProtocol = make(map[string]int)\n\trulesDirection = make(map[string]int)\n\tactiveUsersLastDay = make(map[string]struct{})\n\tembeddedIdpTypes = make(map[string]int)\n\tservicesTargetType = make(map[string]int)\n\tuptime = time.Since(w.startupTime).Seconds()\n\tconnections := w.connManager.GetAllConnectedPeers()\n\tversion = nbversion.NetbirdVersion()\n\n\tcustomDomains, customDomainsValidated, _ := w.dataSource.GetCustomDomainsCounts(ctx)\n\n\tfor _, account := range w.dataSource.GetAllAccounts(ctx) {\n\t\taccounts++\n\n\t\tif account.Settings.PeerLoginExpirationEnabled {\n\t\t\texpirationEnabled++\n\t\t}\n\n\t\tgroups += len(account.Groups)\n\t\tnetworks += len(account.Networks)\n\t\tnetworkResources += len(account.NetworkResources)\n\n\t\tnetworkRouters += len(account.NetworkRouters)\n\t\tfor _, router := range account.NetworkRouters {\n\t\t\tif len(router.PeerGroups) > 0 {\n\t\t\t\tnetworkRoutersWithPG++\n\t\t\t}\n\t\t}\n\n\t\troutes += len(account.Routes)\n\t\tfor _, route := range account.Routes {\n\t\t\tif len(route.PeerGroups) > 0 {\n\t\t\t\troutesWithRGGroups++\n\t\t\t}\n\t\t}\n\t\tnameservers += len(account.NameServerGroups)\n\n\t\tfor _, policy := range account.Policies {\n\t\t\tfor _, rule := range policy.Rules {\n\t\t\t\trules++\n\t\t\t\trulesProtocol[string(rule.Protocol)]++\n\t\t\t\tif rule.Bidirectional {\n\t\t\t\t\trulesDirection[\"bidirectional\"]++\n\t\t\t\t} else {\n\t\t\t\t\trulesDirection[\"oneway\"]++\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(policy.SourcePostureChecks) > 0 {\n\t\t\t\trulesWithSrcPostureChecks++\n\t\t\t}\n\t\t}\n\n\t\tpostureChecks += len(account.PostureChecks)\n\n\t\tfor _, user := range account.Users {\n\t\t\tif user.IsServiceUser {\n\t\t\t\tserviceUsers++\n\t\t\t} else {\n\t\t\t\tusers++\n\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t}\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpats += len(user.PATs)\n\t\t}\n\n\t\tfor _, key := range account.SetupKeys {\n\t\t\tsetupKeysUsage += key.UsedTimes\n\t\t\tif key.Ephemeral {\n\t\t\t\tephemeralPeersSKs++\n\t\t\t\tephemeralPeersSKUsage += key.UsedTimes\n\t\t\t}\n\t\t}\n\n\t\tfor _, peer := range account.Peers {\n\t\t\tpeers++\n\n\t\t\tif peer.SSHEnabled || peer.Meta.Flags.ServerSSHAllowed {\n\t\t\t\tpeersSSHEnabled++\n\t\t\t}\n\n\t\t\tif peer.Meta.Flags.RosenpassEnabled {\n\t\t\t\trosenpassEnabled++\n\t\t\t}\n\n\t\t\tif peer.UserID != \"\" {\n\t\t\t\tuserPeers++\n\t\t\t}\n\n\t\t\tosKey := strings.ToLower(fmt.Sprintf(\"peer_os_%s\", peer.Meta.GoOS))\n\t\t\tosCount := osPeers[osKey]\n\t\t\tosPeers[osKey] = osCount + 1\n\n\t\t\tif peer.Meta.UIVersion != \"\" {\n\t\t\t\tuiClient++\n\t\t\t\tuiOSKey := strings.ToLower(fmt.Sprintf(\"ui_client_os_%s\", peer.Meta.GoOS))\n\t\t\t\tosUICount := osUIClients[uiOSKey]\n\t\t\t\tosUIClients[uiOSKey] = osUICount + 1\n\t\t\t}\n\n\t\t\t_, connected := connections[peer.ID]\n\t\t\tif connected || peer.Status.LastSeen.After(w.lastRun) {\n\t\t\t\tactivePeersLastDay++\n\t\t\t\tif peer.UserID != \"\" {\n\t\t\t\t\tactiveUserPeersLastDay++\n\t\t\t\t\tactiveUsersLastDay[peer.UserID] = struct{}{}\n\t\t\t\t}\n\t\t\t\tosActiveKey := osKey + \"_active\"\n\t\t\t\tosActiveCount := osPeers[osActiveKey]\n\t\t\t\tosPeers[osActiveKey] = osActiveCount + 1\n\t\t\t\tpeerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)\n\t\t\t}\n\t\t}\n\n\t\tfor _, service := range account.Services {\n\t\t\tservices++\n\t\t\tif service.Enabled {\n\t\t\t\tservicesEnabled++\n\t\t\t}\n\t\t\tservicesTargets += len(service.Targets)\n\n\t\t\tswitch rpservice.Status(service.Meta.Status) {\n\t\t\tcase rpservice.StatusActive:\n\t\t\t\tservicesStatusActive++\n\t\t\tcase rpservice.StatusPending:\n\t\t\t\tservicesStatusPending++\n\t\t\tcase rpservice.StatusError, rpservice.StatusCertificateFailed, rpservice.StatusTunnelNotCreated:\n\t\t\t\tservicesStatusError++\n\t\t\t}\n\n\t\t\tfor _, target := range service.Targets {\n\t\t\t\tservicesTargetType[target.TargetType]++\n\t\t\t}\n\n\t\t\tif service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled {\n\t\t\t\tservicesAuthPassword++\n\t\t\t}\n\t\t\tif service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled {\n\t\t\t\tservicesAuthPin++\n\t\t\t}\n\t\t\tif service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled {\n\t\t\t\tservicesAuthOIDC++\n\t\t\t}\n\t\t}\n\t}\n\n\tminActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)\n\tmetricsProperties[\"uptime\"] = uptime\n\tmetricsProperties[\"accounts\"] = accounts\n\tmetricsProperties[\"users\"] = users\n\tmetricsProperties[\"service_users\"] = serviceUsers\n\tmetricsProperties[\"pats\"] = pats\n\tmetricsProperties[\"peers\"] = peers\n\tmetricsProperties[\"peers_ssh_enabled\"] = peersSSHEnabled\n\tmetricsProperties[\"peers_login_expiration_enabled\"] = expirationEnabled\n\tmetricsProperties[\"setup_keys_usage\"] = setupKeysUsage\n\tmetricsProperties[\"ephemeral_peers_setup_keys\"] = ephemeralPeersSKs\n\tmetricsProperties[\"ephemeral_peers_setup_keys_usage\"] = ephemeralPeersSKUsage\n\tmetricsProperties[\"active_peers_last_day\"] = activePeersLastDay\n\tmetricsProperties[\"active_user_peers_last_day\"] = activeUserPeersLastDay\n\tmetricsProperties[\"active_users_last_day\"] = len(activeUsersLastDay)\n\tmetricsProperties[\"user_peers\"] = userPeers\n\tmetricsProperties[\"rules\"] = rules\n\tmetricsProperties[\"rules_with_src_posture_checks\"] = rulesWithSrcPostureChecks\n\tmetricsProperties[\"posture_checks\"] = postureChecks\n\tmetricsProperties[\"groups\"] = groups\n\tmetricsProperties[\"networks\"] = networks\n\tmetricsProperties[\"network_resources\"] = networkResources\n\tmetricsProperties[\"network_routers\"] = networkRouters\n\tmetricsProperties[\"network_routers_with_groups\"] = networkRoutersWithPG\n\tmetricsProperties[\"routes\"] = routes\n\tmetricsProperties[\"routes_with_routing_groups\"] = routesWithRGGroups\n\tmetricsProperties[\"nameservers\"] = nameservers\n\tmetricsProperties[\"version\"] = version\n\tmetricsProperties[\"min_active_peer_version\"] = minActivePeerVersion\n\tmetricsProperties[\"max_active_peer_version\"] = maxActivePeerVersion\n\tmetricsProperties[\"ui_clients\"] = uiClient\n\tmetricsProperties[\"idp_manager\"] = w.idpManager\n\tmetricsProperties[\"store_engine\"] = w.dataSource.GetStoreEngine()\n\tmetricsProperties[\"rosenpass_enabled\"] = rosenpassEnabled\n\tmetricsProperties[\"local_users_count\"] = localUsers\n\tmetricsProperties[\"idp_users_count\"] = idpUsers\n\tmetricsProperties[\"embedded_idp_count\"] = len(embeddedIdpTypes)\n\n\tmetricsProperties[\"services\"] = services\n\tmetricsProperties[\"services_enabled\"] = servicesEnabled\n\tmetricsProperties[\"services_targets\"] = servicesTargets\n\tmetricsProperties[\"services_status_active\"] = servicesStatusActive\n\tmetricsProperties[\"services_status_pending\"] = servicesStatusPending\n\tmetricsProperties[\"services_status_error\"] = servicesStatusError\n\tmetricsProperties[\"services_auth_password\"] = servicesAuthPassword\n\tmetricsProperties[\"services_auth_pin\"] = servicesAuthPin\n\tmetricsProperties[\"services_auth_oidc\"] = servicesAuthOIDC\n\tmetricsProperties[\"custom_domains\"] = customDomains\n\tmetricsProperties[\"custom_domains_validated\"] = customDomainsValidated\n\n\tfor targetType, count := range servicesTargetType {\n\t\tmetricsProperties[\"services_target_type_\"+targetType] = count\n\t}\n\n\tfor idpType, count := range embeddedIdpTypes {\n\t\tmetricsProperties[\"embedded_idp_users_\"+idpType] = count\n\t}\n\n\tfor protocol, count := range rulesProtocol {\n\t\tmetricsProperties[\"rules_protocol_\"+protocol] = count\n\t}\n\n\tfor direction, count := range rulesDirection {\n\t\tmetricsProperties[\"rules_direction_\"+direction] = count\n\t}\n\n\tfor os, count := range osPeers {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tfor os, count := range osUIClients {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tmetricsProperties[\"metric_generation_time\"] = time.Since(start).Milliseconds()\n\n\treturn metricsProperties\n}\n\nfunc getAPIKey(ctx context.Context) (string, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\tdefer cancel()\n\n\thttpClient := http.Client{}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+\"/GetToken\", nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create request for metrics public api token %v\", err)\n\t}\n\tresponse, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to request metrics public api token %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing metrics token response body: %v\", err)\n\t\t}\n\t}()\n\n\tif response.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"unable to retrieve metrics token, statusCode %d\", response.StatusCode)\n\t}\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't get metrics token response; %v\", err)\n\t}\n\n\tvar tokenResponse getTokenResponse\n\n\terr = json.Unmarshal(body, &tokenResponse)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't parse metrics public api token; %v\", err)\n\t}\n\n\treturn tokenResponse.PublicAPIToken, nil\n}\n\nfunc buildMetricsPayload(payload pushPayload) (string, error) {\n\tstr, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to marshal metrics payload, got err: %v\", err)\n\t}\n\treturn string(str), nil\n}\n\nfunc createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, context.CancelFunc, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\treqURL := endpoint\n\n\tpayload := strings.NewReader(payloadStr)\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", reqURL, payload)\n\tif err != nil {\n\t\tcancel()\n\t\treturn nil, nil, err\n\t}\n\treq.Header.Add(\"content-type\", \"application/json\")\n\n\treturn req, cancel, nil\n}\n\n// extractIdpType extracts the IdP type from a Dex connector ID.\n// Connector IDs are formatted as \" - \" (e.g., \"okta-abc123\", \"zitadel-xyz\").\n// Returns the type prefix, or \"oidc\" if no known prefix is found.\nfunc extractIdpType(connectorID string) string {\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn \"oidc\"\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}\n\nfunc getMinMaxVersion(inputList []string) (string, string) {\n\tversions := make([]*version.Version, 0)\n\n\tfor _, raw := range inputList {\n\t\tif raw != \"\" && nbversion.SemverRegexp.MatchString(raw) {\n\t\t\tv, err := version.NewVersion(raw)\n\t\t\tif err == nil {\n\t\t\t\tversions = append(versions, v)\n\t\t\t}\n\t\t}\n\t}\n\n\ttargetIndex := 1\n\tl := len(versions)\n\n\tswitch l {\n\tcase 0:\n\t\treturn \"\", \"\"\n\tcase targetIndex:\n\t\tv := versions[targetIndex-1].String()\n\t\treturn v, v\n\tdefault:\n\t\tsort.Sort(version.Collection(versions))\n\t\treturn versions[targetIndex-1].String(), versions[l-1].String()\n\t}\n}\n","structuredPatch":[{"oldStart":533,"oldLines":7,"newStart":533,"newLines":7,"lines":[" func extractIdpType(connectorID string) string {"," idx := strings.LastIndex(connectorID, \"-\")"," if idx <= 0 {","- return \"oidc\"","+ return strings.ToLower(connectorID)"," }"," return strings.ToLower(connectorID[:idx])"," }"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"acd857eb-c4fd-48d7-b7c0-8ac057fd0ad9"} +{"parentUuid":"859eb881-9c3d-400f-a35b-57f269748766","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_0171gpGrTYAzK4DV2P4pxSgD","type":"message","role":"assistant","content":[{"type":"text","text":"Now let me add tests. Let me check for existing test files:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":354,"cache_read_input_tokens":113219,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":354},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhqJGwFTtnHpg2kiMdmZ","type":"assistant","uuid":"c801e195-d94b-42f8-b0ec-3f46c0d06f19","timestamp":"2026-03-04T09:51:23.902Z"} +{"parentUuid":"c801e195-d94b-42f8-b0ec-3f46c0d06f19","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_0171gpGrTYAzK4DV2P4pxSgD","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_013jxBAK3DWbQ3ausS6bDZAp","name":"Glob","input":{"pattern":"management/server/metrics/*_test.go"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":354,"cache_read_input_tokens":113219,"output_tokens":76,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":354,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqJGwFTtnHpg2kiMdmZ","type":"assistant","uuid":"de3d38a9-1b93-4bc5-8240-73486cc1d9d5","timestamp":"2026-03-04T09:51:24.140Z"} +{"parentUuid":"de3d38a9-1b93-4bc5-8240-73486cc1d9d5","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Glob","command":"callback"},"parentToolUseID":"toolu_013jxBAK3DWbQ3ausS6bDZAp","toolUseID":"toolu_013jxBAK3DWbQ3ausS6bDZAp","timestamp":"2026-03-04T09:51:25.020Z","uuid":"0ce80ed1-0929-4e1e-8d53-dc8fdc45c6b2"} +{"parentUuid":"de3d38a9-1b93-4bc5-8240-73486cc1d9d5","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_013jxBAK3DWbQ3ausS6bDZAp","type":"tool_result","content":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go"}]},"uuid":"f476908c-0f56-4da5-9c86-50b8e2f1886a","timestamp":"2026-03-04T09:51:25.019Z","toolUseResult":{"filenames":["/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go"],"durationMs":876,"numFiles":1,"truncated":false},"sourceToolAssistantUUID":"de3d38a9-1b93-4bc5-8240-73486cc1d9d5"} +{"parentUuid":"f476908c-0f56-4da5-9c86-50b8e2f1886a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_0165KLkQc3G2MGuYbwc38xoh","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01FdcVFbbRQ7tfrJFhMczjaA","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":119,"cache_read_input_tokens":113573,"output_tokens":84,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":119,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqK4fLQozeM4cjrjZTR","type":"assistant","uuid":"8081bfc9-c3dd-49e7-aa1a-ffb082c9f12a","timestamp":"2026-03-04T09:51:33.992Z"} +{"parentUuid":"8081bfc9-c3dd-49e7-aa1a-ffb082c9f12a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01FdcVFbbRQ7tfrJFhMczjaA","toolUseID":"toolu_01FdcVFbbRQ7tfrJFhMczjaA","timestamp":"2026-03-04T09:51:34.071Z","uuid":"efcbc021-0c6e-4dea-9ed3-e33e86f10672"} +{"parentUuid":"8081bfc9-c3dd-49e7-aa1a-ffb082c9f12a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01FdcVFbbRQ7tfrJFhMczjaA","type":"tool_result","content":" 1→package metrics\n 2→\n 3→import (\n 4→\t\"context\"\n 5→\t\"testing\"\n 6→\n 7→\tnbdns \"github.com/netbirdio/netbird/dns\"\n 8→\t\"github.com/netbirdio/netbird/idp/dex\"\n 9→\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n 10→\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n 11→\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n 12→\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n 13→\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n 14→\t\"github.com/netbirdio/netbird/management/server/posture\"\n 15→\t\"github.com/netbirdio/netbird/management/server/types\"\n 16→\t\"github.com/netbirdio/netbird/route\"\n 17→)\n 18→\n 19→type mockDatasource struct{}\n 20→\n 21→// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\n 22→func (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n 23→\treturn map[string]struct{}{\n 24→\t\t\"1\": {},\n 25→\t}\n 26→}\n 27→\n 28→// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\n 29→func (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n 30→\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n 31→\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n 32→\treturn []*types.Account{\n 33→\t\t{\n 34→\t\t\tId: \"1\",\n 35→\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n 36→\t\t\tSetupKeys: map[string]*types.SetupKey{\n 37→\t\t\t\t\"1\": {\n 38→\t\t\t\t\tId: \"1\",\n 39→\t\t\t\t\tEphemeral: true,\n 40→\t\t\t\t\tUsedTimes: 1,\n 41→\t\t\t\t},\n 42→\t\t\t},\n 43→\t\t\tGroups: map[string]*types.Group{\n 44→\t\t\t\t\"1\": {},\n 45→\t\t\t\t\"2\": {},\n 46→\t\t\t},\n 47→\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n 48→\t\t\t\t\"1\": {},\n 49→\t\t\t},\n 50→\t\t\tPeers: map[string]*nbpeer.Peer{\n 51→\t\t\t\t\"1\": {\n 52→\t\t\t\t\tID: \"1\",\n 53→\t\t\t\t\tUserID: \"test\",\n 54→\t\t\t\t\tSSHEnabled: false,\n 55→\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n 56→\t\t\t\t},\n 57→\t\t\t},\n 58→\t\t\tPolicies: []*types.Policy{\n 59→\t\t\t\t{\n 60→\t\t\t\t\tRules: []*types.PolicyRule{\n 61→\t\t\t\t\t\t{\n 62→\t\t\t\t\t\t\tBidirectional: true,\n 63→\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n 64→\t\t\t\t\t\t},\n 65→\t\t\t\t\t},\n 66→\t\t\t\t},\n 67→\t\t\t\t{\n 68→\t\t\t\t\tRules: []*types.PolicyRule{\n 69→\t\t\t\t\t\t{\n 70→\t\t\t\t\t\t\tBidirectional: false,\n 71→\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n 72→\t\t\t\t\t\t},\n 73→\t\t\t\t\t},\n 74→\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n 75→\t\t\t\t},\n 76→\t\t\t},\n 77→\t\t\tRoutes: map[route.ID]*route.Route{\n 78→\t\t\t\t\"1\": {\n 79→\t\t\t\t\tID: \"1\",\n 80→\t\t\t\t\tPeerGroups: make([]string, 1),\n 81→\t\t\t\t},\n 82→\t\t\t},\n 83→\t\t\tPostureChecks: []*posture.Checks{\n 84→\t\t\t\t{\n 85→\t\t\t\t\tID: \"1\",\n 86→\t\t\t\t\tName: \"test\",\n 87→\t\t\t\t\tChecks: posture.ChecksDefinition{\n 88→\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n 89→\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n 90→\t\t\t\t\t\t},\n 91→\t\t\t\t\t},\n 92→\t\t\t\t},\n 93→\t\t\t\t{\n 94→\t\t\t\t\tID: \"2\",\n 95→\t\t\t\t\tName: \"tes2\",\n 96→\t\t\t\t\tChecks: posture.ChecksDefinition{\n 97→\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n 98→\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n 99→\t\t\t\t\t\t},\n 100→\t\t\t\t\t},\n 101→\t\t\t\t},\n 102→\t\t\t},\n 103→\t\t\tUsers: map[string]*types.User{\n 104→\t\t\t\t\"1\": {\n 105→\t\t\t\t\tId: \"1\",\n 106→\t\t\t\t\tIsServiceUser: true,\n 107→\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n 108→\t\t\t\t\t\t\"1\": {},\n 109→\t\t\t\t\t},\n 110→\t\t\t\t},\n 111→\t\t\t\tlocalUserID: {\n 112→\t\t\t\t\tId: localUserID,\n 113→\t\t\t\t\tIsServiceUser: false,\n 114→\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n 115→\t\t\t\t\t\t\"1\": {},\n 116→\t\t\t\t\t},\n 117→\t\t\t\t},\n 118→\t\t\t},\n 119→\t\t\tServices: []*rpservice.Service{\n 120→\t\t\t\t{\n 121→\t\t\t\t\tID: \"svc1\",\n 122→\t\t\t\t\tEnabled: true,\n 123→\t\t\t\t\tTargets: []*rpservice.Target{\n 124→\t\t\t\t\t\t{TargetType: \"peer\"},\n 125→\t\t\t\t\t\t{TargetType: \"host\"},\n 126→\t\t\t\t\t},\n 127→\t\t\t\t\tAuth: rpservice.AuthConfig{\n 128→\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n 129→\t\t\t\t\t},\n 130→\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n 131→\t\t\t\t},\n 132→\t\t\t\t{\n 133→\t\t\t\t\tID: \"svc2\",\n 134→\t\t\t\t\tEnabled: false,\n 135→\t\t\t\t\tTargets: []*rpservice.Target{\n 136→\t\t\t\t\t\t{TargetType: \"domain\"},\n 137→\t\t\t\t\t},\n 138→\t\t\t\t\tAuth: rpservice.AuthConfig{\n 139→\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n 140→\t\t\t\t\t},\n 141→\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n 142→\t\t\t\t},\n 143→\t\t\t},\n 144→\t\t},\n 145→\t\t{\n 146→\t\t\tId: \"2\",\n 147→\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n 148→\t\t\tSetupKeys: map[string]*types.SetupKey{\n 149→\t\t\t\t\"1\": {\n 150→\t\t\t\t\tId: \"1\",\n 151→\t\t\t\t\tEphemeral: true,\n 152→\t\t\t\t\tUsedTimes: 1,\n 153→\t\t\t\t},\n 154→\t\t\t},\n 155→\t\t\tGroups: map[string]*types.Group{\n 156→\t\t\t\t\"1\": {},\n 157→\t\t\t\t\"2\": {},\n 158→\t\t\t},\n 159→\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n 160→\t\t\t\t\"1\": {},\n 161→\t\t\t},\n 162→\t\t\tPeers: map[string]*nbpeer.Peer{\n 163→\t\t\t\t\"1\": {\n 164→\t\t\t\t\tID: \"1\",\n 165→\t\t\t\t\tUserID: \"test\",\n 166→\t\t\t\t\tSSHEnabled: true,\n 167→\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n 168→\t\t\t\t},\n 169→\t\t\t},\n 170→\t\t\tPolicies: []*types.Policy{\n 171→\t\t\t\t{\n 172→\t\t\t\t\tRules: []*types.PolicyRule{\n 173→\t\t\t\t\t\t{\n 174→\t\t\t\t\t\t\tBidirectional: true,\n 175→\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n 176→\t\t\t\t\t\t},\n 177→\t\t\t\t\t},\n 178→\t\t\t\t},\n 179→\t\t\t\t{\n 180→\t\t\t\t\tRules: []*types.PolicyRule{\n 181→\t\t\t\t\t\t{\n 182→\t\t\t\t\t\t\tBidirectional: false,\n 183→\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n 184→\t\t\t\t\t\t},\n 185→\t\t\t\t\t},\n 186→\t\t\t\t},\n 187→\t\t\t},\n 188→\t\t\tRoutes: map[route.ID]*route.Route{\n 189→\t\t\t\t\"1\": {\n 190→\t\t\t\t\tID: \"1\",\n 191→\t\t\t\t\tPeerGroups: make([]string, 1),\n 192→\t\t\t\t},\n 193→\t\t\t},\n 194→\t\t\tUsers: map[string]*types.User{\n 195→\t\t\t\t\"1\": {\n 196→\t\t\t\t\tId: \"1\",\n 197→\t\t\t\t\tIsServiceUser: true,\n 198→\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n 199→\t\t\t\t\t\t\"1\": {},\n 200→\t\t\t\t\t},\n 201→\t\t\t\t},\n 202→\t\t\t\tidpUserID: {\n 203→\t\t\t\t\tId: idpUserID,\n 204→\t\t\t\t\tIsServiceUser: false,\n 205→\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n 206→\t\t\t\t\t\t\"1\": {},\n 207→\t\t\t\t\t},\n 208→\t\t\t\t},\n 209→\t\t\t},\n 210→\t\t\tNetworks: []*networkTypes.Network{\n 211→\t\t\t\t{\n 212→\t\t\t\t\tID: \"1\",\n 213→\t\t\t\t\tAccountID: \"1\",\n 214→\t\t\t\t},\n 215→\t\t\t},\n 216→\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n 217→\t\t\t\t{\n 218→\t\t\t\t\tID: \"1\",\n 219→\t\t\t\t\tAccountID: \"1\",\n 220→\t\t\t\t\tNetworkID: \"1\",\n 221→\t\t\t\t},\n 222→\t\t\t\t{\n 223→\t\t\t\t\tID: \"2\",\n 224→\t\t\t\t\tAccountID: \"1\",\n 225→\t\t\t\t\tNetworkID: \"1\",\n 226→\t\t\t\t},\n 227→\t\t\t},\n 228→\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n 229→\t\t\t\t{\n 230→\t\t\t\t\tID: \"1\",\n 231→\t\t\t\t\tAccountID: \"1\",\n 232→\t\t\t\t\tNetworkID: \"1\",\n 233→\t\t\t\t},\n 234→\t\t\t},\n 235→\t\t},\n 236→\t}\n 237→}\n 238→\n 239→// GetStoreEngine returns FileStoreEngine\n 240→func (mockDatasource) GetStoreEngine() types.Engine {\n 241→\treturn types.FileStoreEngine\n 242→}\n 243→\n 244→// GetCustomDomainsCounts returns test custom domain counts.\n 245→func (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n 246→\treturn 3, 2, nil\n 247→}\n 248→\n 249→// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\n 250→func TestGenerateProperties(t *testing.T) {\n 251→\tds := mockDatasource{}\n 252→\tworker := Worker{\n 253→\t\tdataSource: ds,\n 254→\t\tconnManager: ds,\n 255→\t\tidpManager: EmbeddedType,\n 256→\t}\n 257→\n 258→\tproperties := worker.generateProperties(context.Background())\n 259→\n 260→\tif properties[\"accounts\"] != 2 {\n 261→\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n 262→\t}\n 263→\tif properties[\"peers\"] != 2 {\n 264→\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n 265→\t}\n 266→\tif properties[\"routes\"] != 2 {\n 267→\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n 268→\t}\n 269→\tif properties[\"networks\"] != 1 {\n 270→\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n 271→\t}\n 272→\tif properties[\"network_resources\"] != 2 {\n 273→\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n 274→\t}\n 275→\tif properties[\"network_routers\"] != 1 {\n 276→\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n 277→\t}\n 278→\tif properties[\"rules\"] != 4 {\n 279→\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n 280→\t}\n 281→\tif properties[\"users\"] != 2 {\n 282→\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n 283→\t}\n 284→\tif properties[\"setup_keys_usage\"] != 2 {\n 285→\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n 286→\t}\n 287→\tif properties[\"pats\"] != 4 {\n 288→\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n 289→\t}\n 290→\tif properties[\"peers_ssh_enabled\"] != 2 {\n 291→\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n 292→\t}\n 293→\tif properties[\"routes_with_routing_groups\"] != 2 {\n 294→\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n 295→\t}\n 296→\tif properties[\"rules_protocol_tcp\"] != 4 {\n 297→\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n 298→\t}\n 299→\tif properties[\"rules_direction_oneway\"] != 2 {\n 300→\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n 301→\t}\n 302→\n 303→\tif properties[\"active_peers_last_day\"] != 2 {\n 304→\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n 305→\t}\n 306→\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n 307→\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n 308→\t}\n 309→\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n 310→\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n 311→\t}\n 312→\n 313→\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n 314→\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n 315→\t}\n 316→\n 317→\tif properties[\"service_users\"] != 2 {\n 318→\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n 319→\t}\n 320→\n 321→\tif properties[\"peer_os_linux\"] != 2 {\n 322→\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n 323→\t}\n 324→\n 325→\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n 326→\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n 327→\t}\n 328→\n 329→\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n 330→\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n 331→\t}\n 332→\n 333→\tif properties[\"nameservers\"] != 2 {\n 334→\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n 335→\t}\n 336→\n 337→\tif properties[\"groups\"] != 4 {\n 338→\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n 339→\t}\n 340→\n 341→\tif properties[\"user_peers\"] != 2 {\n 342→\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n 343→\t}\n 344→\n 345→\tif properties[\"store_engine\"] != types.FileStoreEngine {\n 346→\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n 347→\t}\n 348→\n 349→\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n 350→\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n 351→\t}\n 352→\n 353→\tif properties[\"posture_checks\"] != 2 {\n 354→\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n 355→\t}\n 356→\n 357→\tif properties[\"rosenpass_enabled\"] != 1 {\n 358→\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n 359→\t}\n 360→\n 361→\tif properties[\"active_user_peers_last_day\"] != 2 {\n 362→\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n 363→\t}\n 364→\n 365→\tif properties[\"active_users_last_day\"] != 1 {\n 366→\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n 367→\t}\n 368→\n 369→\tif properties[\"local_users_count\"] != 1 {\n 370→\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n 371→\t}\n 372→\tif properties[\"idp_users_count\"] != 1 {\n 373→\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n 374→\t}\n 375→\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n 376→\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n 377→\t}\n 378→\tif properties[\"embedded_idp_count\"] != 1 {\n 379→\t\tt.Errorf(\"expected 1 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n 380→\t}\n 381→\n 382→\tif properties[\"services\"] != 2 {\n 383→\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n 384→\t}\n 385→\tif properties[\"services_enabled\"] != 1 {\n 386→\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n 387→\t}\n 388→\tif properties[\"services_targets\"] != 3 {\n 389→\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n 390→\t}\n 391→\tif properties[\"services_status_active\"] != 1 {\n 392→\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n 393→\t}\n 394→\tif properties[\"services_status_pending\"] != 1 {\n 395→\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n 396→\t}\n 397→\tif properties[\"services_status_error\"] != 0 {\n 398→\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n 399→\t}\n 400→\tif properties[\"services_target_type_peer\"] != 1 {\n 401→\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n 402→\t}\n 403→\tif properties[\"services_target_type_host\"] != 1 {\n 404→\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n 405→\t}\n 406→\tif properties[\"services_target_type_domain\"] != 1 {\n 407→\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n 408→\t}\n 409→\tif properties[\"services_auth_password\"] != 1 {\n 410→\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n 411→\t}\n 412→\tif properties[\"services_auth_oidc\"] != 1 {\n 413→\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n 414→\t}\n 415→\tif properties[\"services_auth_pin\"] != 0 {\n 416→\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n 417→\t}\n 418→\tif properties[\"custom_domains\"] != int64(3) {\n 419→\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n 420→\t}\n 421→\tif properties[\"custom_domains_validated\"] != int64(2) {\n 422→\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n 423→\t}\n 424→}\n 425→\n 426→func TestExtractIdpType(t *testing.T) {\n 427→\ttests := []struct {\n 428→\t\tconnectorID string\n 429→\t\texpected string\n 430→\t}{\n 431→\t\t{\"okta-abc123def\", \"okta\"},\n 432→\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n 433→\t\t{\"entra-xyz789\", \"entra\"},\n 434→\t\t{\"google-abc123\", \"google\"},\n 435→\t\t{\"pocketid-abc123\", \"pocketid\"},\n 436→\t\t{\"microsoft-abc123\", \"microsoft\"},\n 437→\t\t{\"authentik-abc123\", \"authentik\"},\n 438→\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n 439→\t\t{\"local\", \"oidc\"},\n 440→\t\t{\"\", \"oidc\"},\n 441→\t}\n 442→\n 443→\tfor _, tt := range tests {\n 444→\t\tt.Run(tt.connectorID, func(t *testing.T) {\n 445→\t\t\tresult := extractIdpType(tt.connectorID)\n 446→\t\t\tif result != tt.expected {\n 447→\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n 448→\t\t\t}\n 449→\t\t})\n 450→\t}\n 451→}\n 452→"}]},"uuid":"b2874b03-7167-449f-8801-eac6f40c5ee6","timestamp":"2026-03-04T09:51:34.070Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","content":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n","numLines":452,"startLine":1,"totalLines":452}},"sourceToolAssistantUUID":"8081bfc9-c3dd-49e7-aa1a-ffb082c9f12a"} +{"parentUuid":"b2874b03-7167-449f-8801-eac6f40c5ee6","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_013v5NKsAmvw5MM4P34iqUhR","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The existing test at line 439 expects `extractIdpType(\"local\")` to return `\"oidc\"`. With my fix, it will return `\"local\"`. I need to update this test case.\n\nAlso, the `TestGenerateProperties` test at line 378 expects `embedded_idp_count` to be 1. With my fix, it will be 2 (local + zitadel). I need to update that too.\n\nAnd I should add a test for `embedded_idp_users_local` being 1.\n\nLet me update the tests:\n1. Fix `extractIdpType(\"local\")` expected from `\"oidc\"` to `\"local\"` \n\n\n2. For the empty string case, my fix would return `\"\"` since there's no dash to split on, which is a bit odd but probably fine since empty connector IDs shouldn't occur in practice anyway.\n\nActually, I realize the test expects `extractIdpType(\"\")` to return `\"oidc\"`, but my change would return `\"\"` instead. That would create a weird metric key. Let me add a guard clause to handle the empty case properly.\n\nAdding guard clause for empty string... Since `idpID` is guaranteed to be non-empty when `extractIdpType` is called, the empty string check is just defensive programming. Now I need to update the test cases to reflect the expected behavior: `{\"local\", \"oidc\"}` should map to `{\"local\", \"local\"}`, the empty connector case should return `\"unknown\"`, and I need to verify the embedded IDP count is 2 and add an assertion for the local users metric.","signature":"EvYVCkYICxgCKkDAM9uWWQmVOwP6r9I4spfjj9ZAeLiKArYDToqYFGR0Tl9e89HSo73JGuEfO94fW6YReSnW/BCPjpXDhTiCbzuEEgylaTlfzYX6X5Puk8saDPe2SKVX317d/wv0pyIwYXjVZpHCblOlh/X5TKBP4L+hYkIicFi01uSYUNSP3TgyqsfgdGh3KqeTIG25AX1yKt0UpmXAxPWw5YaAGO9S17jHPNjeTD3Wb9d6YsspfchgQUKwhJf9ITqxlwSGB9YA/cvo2wpHSWCMCQF8bjYB6nrt6CuIoYtsWskBUMv6kSqKaS/lTYkgYNBBcIYHAcLzdWH/ATm5Wkh5jj2ekUOanGDxyCEW8Al5B/67kTR+9bevoaR1D3Ts0vBIl+WIIVTbS3TqezhYxDlj+TJnCbLhXmEdfoKYq36aP+V2i2+kmvsDBgcUv0RlyOZfKSuLitU5XS77+8aXfaoTPT4ooRaKCjU/YLFgwPB0biMILC9cISZgMUZhrO1ZDrBx5FJ4ZkmWCnPjuazQFMsnG5F/VROVHxPqFeCLvqiOZaPHkAOaMg0wKvvQytJtSaN99VvSLfg7To7wyg3q6j88ao3bzHIo6tcR96LyNQ6BKlEYnZ2ZoQa6PtA9ScKx1c1QeA+EUgyNmEZAP0hSwnuwVWn2Hg76L7oVu/b9ucB5AqiCMTUO5oDI+rgCwyvbfw70zrUzbUxAAOIl7/F5dP1oGIQZNAr2LKNCsg3iZQWga0+aKt50LQTQ4s5CLkfgsaQJqyv40+1++ogxS6aqZ2cLvbhgd8hsBEyAgLihXEFnYtn89FQMwDiIdsNve0qw8L3jitrdOREr3t3So4ebAbi+ot9D1A62ZEctaYa3MuWUi4AA7S3aoRFg6nRGbgqgk8RxSsvqQ7p9nEZfOILjAjiwDPyLw042BI9Gg4Ab5ocjF+NzaG4uzyBvB4oouimK+6Nk2RgjvwxaSZgxrcfDv7G/Yjxrjfyq0lWLL5h+moZ+8DF6hZ+71yQeqcRMRpJYokb8QjluV60T4vipp2Ud5pGlphx8qOUEFKFvOaupUyAgSviJl/73gJnbxZ7Zcxf5+KTx1RFnL0lstXJNRp/puWYJUVeoWBgaCkvsFLmVRy8VPz0OoklM1FHysAR7V6YsprJpRIS8NFAt9ODisBeKrb/TcIj//wrZl1rP1fP32EWMCj4X0MG0cmZGEj3+GgWnzrv4IU6btsEJPUUVypR4gIe8IFccr7EdM53hMHq4AhbwewJwrZ+VyeWDhoOIqLAu3yAykgjqDiyXAJbPHevM6JrqBbxQk+b2RZr8FPjlfSoGtLS6nWAlidQPXozE+4QLsNvzZtmgGXfjriHQGdMJz7hbtsExjDfJkC6EAGT8uS9Q5S6XENr5+GYz4mwC8/yNHLIxLUMhR4WNnpYOo7rAc8Gee3Z0dKGWZrFiSAKRDq06BPsruN4HcRjHk+IRoDKQ30qVod9PUeChrZBgoczVr1NB5dCzMOseNOFz7F8WZI6XMXDkl4pir7cmUVucQB6t49FOPeAFSl4dTjyh6WEjIb8lfne0zdFMtyeqNv7onBBEROIOQGX2ArmJ0m+YyY8UMzBxUfi13vlblu+yIxdZ/kacw3PPQnJy8G3RXk0hNnYF1/iZpsYDGmyTJnQ8n8rJGipHMZVEuC1FGqbfjTmXSwdyVfqpMTE/JELHU7uDtRCaMOecPzS6xGMbbtEw3ODUnStaw5lKCl/nCKXez2a9p//LGM281s/xPsb2oDLOmLjhsZUqhuUuR1G3rdk06yqpCj0YpFdtRFyz/9YYfmNEmKo/0Z7iYVJvpatjvG2acRAB5HlvCCtMYd3RvX8N/IwVkjp4waS+5XsvRanx494Sm3TSa+jQn5oR4MMr6mT/yMlxC3sZdn2vmaVnh5rWTzZIzYOdop16geNJRJJL/G1AThARDOHmTEnvKNQA5d9mi0kcCT8y8C+BcWtmAgpRDvhf/DSmb24Omy1JxjN7/bUCpBc+2GLM+wY+ooDABpdVSok8HFKq0rH8P1t9HCCxT6t1Ww+sfiQIHhJHct4BSv0UeWgbzgTuCDWy3hbbFg1IK0rFKuGSeQeUAnafMzCuAgWr2pqKKWrBPkyy5sgqfW2gRmrUPeA5CsaI6CSQqM+xxXMnw3oxCW4sXjkuiCH4I7ZYFWhrP03AcUmwaqHRqNpwV+2wzYD+NjyqIlb6VXFiSHMzw+0bGqlraoeKXtnM8/1abRIhCIn2GtvIXra7GQSptHNS5cLAcseLPXKF76ZiTs3RllCiSlNYPUnLPjEL2PN4cesLd8oyOjm6zhjBPeOPz8Js9peE0eUBOJUoD7VEvJ4mEqoIwBdA4gV2BdhwIgoZbol3X2vRHotuivtvY+h36fUDCQP1lSVYyO8/OgHybMFu22vE1ejk4E0qMvJZkoe/XfIm4jx5KsEC0/fmF/e6farykOp4kHusCrVZHb5hvf03wfQHN3822CHNBCgXqTpV7Pk8QvYWX3jVw+bFQyESBiD7zXZ6RW3uJxUU/sNgaJtacmyvv7/yn7zaAmFnOSN9cWmY984YLGnvFBo4iUFXCU7zdJZqlhNA5zblnX4rVbCv6soNICh6y8uh6+Moz0NqWCsm1mauwXrtVEaIue1VZ7HpTn05koMolHn9Lwon9KAinHxnz4dzEVEfRiY5h/MbDY3kQXgmcJKuhv6RpZZCcyrIGkHl5b+Cuqy332DtmBsUJsc1e4tNtXVDhY72gYlkMBdFlZZS2v2asvqLYZFUcSw8hWH5o1G0dVK3Hy3vxIYn10Al4kd67SQp/GUZEDQPesPDVIE3PnUDFvdPQHF1zsRtiwuJneMyC7bTF4KrV3d7kHVlTGUEgEW0lBe+BNGcYx7IyScPj1LqggbVmrPOjFIEcuIn9qmCHfK6lmTDSgzQAFxoiexsdEply5bE0mcdeSaPpLhVWcZ6n7I8SSqYm4J6sW0ZUIU7eDnMDDEWoSpyiycdYrqbRcI9iSE8qcgk3XadfptenjGYhe2WdzRUs1rBs0mLF/bJJd1ICAdTlhp13vuKhYBVNan2rMYVHKJoGjE4QTX+BmO08K5Tcpp708jme1vS8dUgF2Ua6A+0YUqscX27SXIQhsAUrk6ynTpA0giQjL+f6i6MKqOsU6N7YXwk718oAGvsRYDh906b3DmM/W4NBcyLbvvZcTyqXz48k5IT+76okreG0A+esd0+eb6QjfX5cupje0NWhYNo9S+c+q1PNLfuoPkQ0HUXB046O0EwAJvfe+7JPpbelE5roqOXLJMUFrCQuQcF2yb1fdKCkaCxG81r3OMhS4E5ETQ23VHW1s5R/0yzB7j47CRviOSQh8Tvxzu/MHHx3k2Z6pHT+XJe/2cgzRITAQpwk1V5xOB5BKU3iLv3TF5R27w9eAO7vMWoUNXD0Af6NeX5cylRDBBiw1w4nbi1H2ur6KsCjExDQLycaXb76OXPatdPkgOxotqP9sRG6oZzL0ezYdugmqtYGF/8iPUDvJILqORJ8+hiuP/deXUBWxaRTpOg+2DiFSuK7IvogDd9QEub8BPiEfF44jrZRSo6ZEmmu1NR9LeAB1sPvU/Wd4QtqacPN2JWaFHAzJwO3b3XkJ5o+nSn91/ZTYM38Mrl+lSLjiYxNLNIXoX6t6fcnM3zG9ubNYmy/Ubd8mzcdWT0G4zJ6u+sbwt4ajg7ddFQ6V47usnApboQv9Y7lAi1Rjav//iAhJWvgbSCwh4g6BgB"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":6449,"cache_read_input_tokens":113692,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":6449},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhqKPVv8WLkvwtpJk1bY","type":"assistant","uuid":"f85e2b6e-fac5-461f-9ef8-4689dd306ee9","timestamp":"2026-03-04T09:51:54.013Z"} +{"parentUuid":"f85e2b6e-fac5-461f-9ef8-4689dd306ee9","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_013v5NKsAmvw5MM4P34iqUhR","type":"message","role":"assistant","content":[{"type":"text","text":"The existing tests need updating. The test at line 439 expects `extractIdpType(\"local\")` → `\"oidc\"` — that's exactly the bug. And `embedded_idp_count` at line 378 expects 1, but it should now be 2 (local + zitadel)."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":6449,"cache_read_input_tokens":113692,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":6449},"output_tokens":8,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhqKPVv8WLkvwtpJk1bY","type":"assistant","uuid":"dc916b7d-a928-4db3-a20d-2ffe35e6cf54","timestamp":"2026-03-04T09:51:54.014Z"} +{"parentUuid":"dc916b7d-a928-4db3-a20d-2ffe35e6cf54","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_013v5NKsAmvw5MM4P34iqUhR","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01HHCrfwBDHxrFP2ndUMhdYL","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go","old_string":"func extractIdpType(connectorID string) string {\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn strings.ToLower(connectorID)\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}","new_string":"func extractIdpType(connectorID string) string {\n\tif connectorID == \"\" {\n\t\treturn \"unknown\"\n\t}\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn strings.ToLower(connectorID)\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":6449,"cache_read_input_tokens":113692,"output_tokens":1225,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":6449,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqKPVv8WLkvwtpJk1bY","type":"assistant","uuid":"e671a191-27fc-4bd0-b031-6a45a570f389","timestamp":"2026-03-04T09:51:56.639Z"} +{"parentUuid":"e671a191-27fc-4bd0-b031-6a45a570f389","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_01HHCrfwBDHxrFP2ndUMhdYL","toolUseID":"toolu_01HHCrfwBDHxrFP2ndUMhdYL","timestamp":"2026-03-04T09:52:08.888Z","uuid":"8141a2f4-68a4-4e70-90aa-c8ad68e79caa"} +{"parentUuid":"e671a191-27fc-4bd0-b031-6a45a570f389","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01HHCrfwBDHxrFP2ndUMhdYL","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go has been updated successfully."}]},"uuid":"5f79abe1-9477-4080-b643-d87b1a1459b5","timestamp":"2026-03-04T09:52:08.887Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go","oldString":"// Package metrics gather anonymous information about the usage of NetBird management\npackage metrics\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\tnbversion \"github.com/netbirdio/netbird/version\"\n)\n\nconst (\n\t// PayloadEvent identifies an event type\n\tPayloadEvent = \"self-hosted stats\"\n\t// payloadEndpoint metrics defaultEndpoint to send anonymous data\n\tpayloadEndpoint = \"https://metrics.netbird.io\"\n\t// defaultPushInterval default interval to push metrics\n\tdefaultPushInterval = 12 * time.Hour\n\t// requestTimeout http request timeout\n\trequestTimeout = 45 * time.Second\n\tEmbeddedType = \"embedded\"\n)\n\ntype getTokenResponse struct {\n\tPublicAPIToken string `json:\"public_api_token\"`\n}\n\ntype pushPayload struct {\n\tAPIKey string `json:\"api_key\"`\n\tDistinctID string `json:\"distinct_id\"`\n\tEvent string `json:\"event\"`\n\tProperties properties `json:\"properties\"`\n\tTimestamp time.Time `json:\"timestamp\"`\n}\n\n// properties metrics to push\ntype properties map[string]interface{}\n\n// DataSource metric data source\ntype DataSource interface {\n\tGetAllAccounts(ctx context.Context) []*types.Account\n\tGetStoreEngine() types.Engine\n\tGetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)\n}\n\n// ConnManager peer connection manager that holds state for current active connections\ntype ConnManager interface {\n\tGetAllConnectedPeers() map[string]struct{}\n}\n\n// Worker metrics collector and pusher\ntype Worker struct {\n\tctx context.Context\n\tid string\n\tidpManager string\n\tdataSource DataSource\n\tconnManager ConnManager\n\tstartupTime time.Time\n\tlastRun time.Time\n}\n\n// NewWorker returns a metrics worker\nfunc NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {\n\tcurrentTime := time.Now()\n\treturn &Worker{\n\t\tctx: ctx,\n\t\tid: id,\n\t\tidpManager: idpManager,\n\t\tdataSource: dataSource,\n\t\tconnManager: connManager,\n\t\tstartupTime: currentTime,\n\t\tlastRun: currentTime,\n\t}\n}\n\n// Run runs the metrics worker\nfunc (w *Worker) Run(ctx context.Context) {\n\tinterval := getMetricsInterval(ctx)\n\n\tpushTicker := time.NewTicker(interval)\n\tfor {\n\t\tselect {\n\t\tcase <-w.ctx.Done():\n\t\t\treturn\n\t\tcase <-pushTicker.C:\n\t\t\terr := w.sendMetrics(ctx)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithContext(ctx).Error(err)\n\t\t\t}\n\t\t\tw.lastRun = time.Now()\n\t\t}\n\t}\n}\n\nfunc getMetricsInterval(ctx context.Context) time.Duration {\n\tinterval := defaultPushInterval\n\tif os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") != \"\" {\n\t\tnewInterval, err := time.ParseDuration(os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") + \"s\")\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"unable to parse NETBIRD_METRICS_INTERVAL_IN_SECONDS, using default interval %v. Error: %v\", defaultPushInterval, err)\n\t\t} else {\n\t\t\tlog.WithContext(ctx).Infof(\"using NETBIRD_METRICS_INTERVAL_IN_SECONDS %s\", newInterval)\n\t\t\tinterval = newInterval\n\t\t}\n\t}\n\treturn interval\n}\n\nfunc (w *Worker) sendMetrics(ctx context.Context) error {\n\tapiKey, err := getAPIKey(w.ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpayload := w.generatePayload(ctx, apiKey)\n\n\tpayloadString, err := buildMetricsPayload(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thttpClient := http.Client{}\n\n\texportJobReq, cancelCTX, err := createPostRequest(w.ctx, payloadEndpoint+\"/capture/\", payloadString)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create metrics post request %v\", err)\n\t}\n\tdefer cancelCTX()\n\n\tjobResp, err := httpClient.Do(exportJobReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to push metrics %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = jobResp.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing update metrics response body: %v\", err)\n\t\t}\n\t}()\n\n\tif jobResp.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"unable to push anonymous metrics, got statusCode %d\", jobResp.StatusCode)\n\t}\n\n\tlog.WithContext(ctx).Infof(\"sent anonymous metrics, next push will happen in %s. \"+\n\t\t\"You can disable these metrics by running with flag --disable-anonymous-metrics,\"+\n\t\t\" see more information at https://docs.netbird.io/about-netbird/faq#why-and-what-are-the-anonymous-usage-metrics\", getMetricsInterval(ctx))\n\n\treturn nil\n}\n\nfunc (w *Worker) generatePayload(ctx context.Context, apiKey string) pushPayload {\n\tproperties := w.generateProperties(ctx)\n\n\treturn pushPayload{\n\t\tAPIKey: apiKey,\n\t\tDistinctID: w.id,\n\t\tEvent: PayloadEvent,\n\t\tProperties: properties,\n\t\tTimestamp: time.Now(),\n\t}\n}\n\nfunc (w *Worker) generateProperties(ctx context.Context) properties {\n\tvar (\n\t\tuptime float64\n\t\taccounts int\n\t\texpirationEnabled int\n\t\tusers int\n\t\tserviceUsers int\n\t\tpats int\n\t\tpeers int\n\t\tpeersSSHEnabled int\n\t\tsetupKeysUsage int\n\t\tephemeralPeersSKs int\n\t\tephemeralPeersSKUsage int\n\t\tactivePeersLastDay int\n\t\tactiveUserPeersLastDay int\n\t\tosPeers map[string]int\n\t\tactiveUsersLastDay map[string]struct{}\n\t\tuserPeers int\n\t\trules int\n\t\trulesProtocol map[string]int\n\t\trulesDirection map[string]int\n\t\trulesWithSrcPostureChecks int\n\t\tpostureChecks int\n\t\tgroups int\n\t\troutes int\n\t\troutesWithRGGroups int\n\t\tnetworks int\n\t\tnetworkResources int\n\t\tnetworkRouters int\n\t\tnetworkRoutersWithPG int\n\t\tnameservers int\n\t\tuiClient int\n\t\tversion string\n\t\tpeerActiveVersions []string\n\t\tosUIClients map[string]int\n\t\trosenpassEnabled int\n\t\tlocalUsers int\n\t\tidpUsers int\n\t\tembeddedIdpTypes map[string]int\n\t\tservices int\n\t\tservicesEnabled int\n\t\tservicesTargets int\n\t\tservicesStatusActive int\n\t\tservicesStatusPending int\n\t\tservicesStatusError int\n\t\tservicesTargetType map[string]int\n\t\tservicesAuthPassword int\n\t\tservicesAuthPin int\n\t\tservicesAuthOIDC int\n\t)\n\tstart := time.Now()\n\tmetricsProperties := make(properties)\n\tosPeers = make(map[string]int)\n\tosUIClients = make(map[string]int)\n\trulesProtocol = make(map[string]int)\n\trulesDirection = make(map[string]int)\n\tactiveUsersLastDay = make(map[string]struct{})\n\tembeddedIdpTypes = make(map[string]int)\n\tservicesTargetType = make(map[string]int)\n\tuptime = time.Since(w.startupTime).Seconds()\n\tconnections := w.connManager.GetAllConnectedPeers()\n\tversion = nbversion.NetbirdVersion()\n\n\tcustomDomains, customDomainsValidated, _ := w.dataSource.GetCustomDomainsCounts(ctx)\n\n\tfor _, account := range w.dataSource.GetAllAccounts(ctx) {\n\t\taccounts++\n\n\t\tif account.Settings.PeerLoginExpirationEnabled {\n\t\t\texpirationEnabled++\n\t\t}\n\n\t\tgroups += len(account.Groups)\n\t\tnetworks += len(account.Networks)\n\t\tnetworkResources += len(account.NetworkResources)\n\n\t\tnetworkRouters += len(account.NetworkRouters)\n\t\tfor _, router := range account.NetworkRouters {\n\t\t\tif len(router.PeerGroups) > 0 {\n\t\t\t\tnetworkRoutersWithPG++\n\t\t\t}\n\t\t}\n\n\t\troutes += len(account.Routes)\n\t\tfor _, route := range account.Routes {\n\t\t\tif len(route.PeerGroups) > 0 {\n\t\t\t\troutesWithRGGroups++\n\t\t\t}\n\t\t}\n\t\tnameservers += len(account.NameServerGroups)\n\n\t\tfor _, policy := range account.Policies {\n\t\t\tfor _, rule := range policy.Rules {\n\t\t\t\trules++\n\t\t\t\trulesProtocol[string(rule.Protocol)]++\n\t\t\t\tif rule.Bidirectional {\n\t\t\t\t\trulesDirection[\"bidirectional\"]++\n\t\t\t\t} else {\n\t\t\t\t\trulesDirection[\"oneway\"]++\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(policy.SourcePostureChecks) > 0 {\n\t\t\t\trulesWithSrcPostureChecks++\n\t\t\t}\n\t\t}\n\n\t\tpostureChecks += len(account.PostureChecks)\n\n\t\tfor _, user := range account.Users {\n\t\t\tif user.IsServiceUser {\n\t\t\t\tserviceUsers++\n\t\t\t} else {\n\t\t\t\tusers++\n\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t}\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpats += len(user.PATs)\n\t\t}\n\n\t\tfor _, key := range account.SetupKeys {\n\t\t\tsetupKeysUsage += key.UsedTimes\n\t\t\tif key.Ephemeral {\n\t\t\t\tephemeralPeersSKs++\n\t\t\t\tephemeralPeersSKUsage += key.UsedTimes\n\t\t\t}\n\t\t}\n\n\t\tfor _, peer := range account.Peers {\n\t\t\tpeers++\n\n\t\t\tif peer.SSHEnabled || peer.Meta.Flags.ServerSSHAllowed {\n\t\t\t\tpeersSSHEnabled++\n\t\t\t}\n\n\t\t\tif peer.Meta.Flags.RosenpassEnabled {\n\t\t\t\trosenpassEnabled++\n\t\t\t}\n\n\t\t\tif peer.UserID != \"\" {\n\t\t\t\tuserPeers++\n\t\t\t}\n\n\t\t\tosKey := strings.ToLower(fmt.Sprintf(\"peer_os_%s\", peer.Meta.GoOS))\n\t\t\tosCount := osPeers[osKey]\n\t\t\tosPeers[osKey] = osCount + 1\n\n\t\t\tif peer.Meta.UIVersion != \"\" {\n\t\t\t\tuiClient++\n\t\t\t\tuiOSKey := strings.ToLower(fmt.Sprintf(\"ui_client_os_%s\", peer.Meta.GoOS))\n\t\t\t\tosUICount := osUIClients[uiOSKey]\n\t\t\t\tosUIClients[uiOSKey] = osUICount + 1\n\t\t\t}\n\n\t\t\t_, connected := connections[peer.ID]\n\t\t\tif connected || peer.Status.LastSeen.After(w.lastRun) {\n\t\t\t\tactivePeersLastDay++\n\t\t\t\tif peer.UserID != \"\" {\n\t\t\t\t\tactiveUserPeersLastDay++\n\t\t\t\t\tactiveUsersLastDay[peer.UserID] = struct{}{}\n\t\t\t\t}\n\t\t\t\tosActiveKey := osKey + \"_active\"\n\t\t\t\tosActiveCount := osPeers[osActiveKey]\n\t\t\t\tosPeers[osActiveKey] = osActiveCount + 1\n\t\t\t\tpeerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)\n\t\t\t}\n\t\t}\n\n\t\tfor _, service := range account.Services {\n\t\t\tservices++\n\t\t\tif service.Enabled {\n\t\t\t\tservicesEnabled++\n\t\t\t}\n\t\t\tservicesTargets += len(service.Targets)\n\n\t\t\tswitch rpservice.Status(service.Meta.Status) {\n\t\t\tcase rpservice.StatusActive:\n\t\t\t\tservicesStatusActive++\n\t\t\tcase rpservice.StatusPending:\n\t\t\t\tservicesStatusPending++\n\t\t\tcase rpservice.StatusError, rpservice.StatusCertificateFailed, rpservice.StatusTunnelNotCreated:\n\t\t\t\tservicesStatusError++\n\t\t\t}\n\n\t\t\tfor _, target := range service.Targets {\n\t\t\t\tservicesTargetType[target.TargetType]++\n\t\t\t}\n\n\t\t\tif service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled {\n\t\t\t\tservicesAuthPassword++\n\t\t\t}\n\t\t\tif service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled {\n\t\t\t\tservicesAuthPin++\n\t\t\t}\n\t\t\tif service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled {\n\t\t\t\tservicesAuthOIDC++\n\t\t\t}\n\t\t}\n\t}\n\n\tminActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)\n\tmetricsProperties[\"uptime\"] = uptime\n\tmetricsProperties[\"accounts\"] = accounts\n\tmetricsProperties[\"users\"] = users\n\tmetricsProperties[\"service_users\"] = serviceUsers\n\tmetricsProperties[\"pats\"] = pats\n\tmetricsProperties[\"peers\"] = peers\n\tmetricsProperties[\"peers_ssh_enabled\"] = peersSSHEnabled\n\tmetricsProperties[\"peers_login_expiration_enabled\"] = expirationEnabled\n\tmetricsProperties[\"setup_keys_usage\"] = setupKeysUsage\n\tmetricsProperties[\"ephemeral_peers_setup_keys\"] = ephemeralPeersSKs\n\tmetricsProperties[\"ephemeral_peers_setup_keys_usage\"] = ephemeralPeersSKUsage\n\tmetricsProperties[\"active_peers_last_day\"] = activePeersLastDay\n\tmetricsProperties[\"active_user_peers_last_day\"] = activeUserPeersLastDay\n\tmetricsProperties[\"active_users_last_day\"] = len(activeUsersLastDay)\n\tmetricsProperties[\"user_peers\"] = userPeers\n\tmetricsProperties[\"rules\"] = rules\n\tmetricsProperties[\"rules_with_src_posture_checks\"] = rulesWithSrcPostureChecks\n\tmetricsProperties[\"posture_checks\"] = postureChecks\n\tmetricsProperties[\"groups\"] = groups\n\tmetricsProperties[\"networks\"] = networks\n\tmetricsProperties[\"network_resources\"] = networkResources\n\tmetricsProperties[\"network_routers\"] = networkRouters\n\tmetricsProperties[\"network_routers_with_groups\"] = networkRoutersWithPG\n\tmetricsProperties[\"routes\"] = routes\n\tmetricsProperties[\"routes_with_routing_groups\"] = routesWithRGGroups\n\tmetricsProperties[\"nameservers\"] = nameservers\n\tmetricsProperties[\"version\"] = version\n\tmetricsProperties[\"min_active_peer_version\"] = minActivePeerVersion\n\tmetricsProperties[\"max_active_peer_version\"] = maxActivePeerVersion\n\tmetricsProperties[\"ui_clients\"] = uiClient\n\tmetricsProperties[\"idp_manager\"] = w.idpManager\n\tmetricsProperties[\"store_engine\"] = w.dataSource.GetStoreEngine()\n\tmetricsProperties[\"rosenpass_enabled\"] = rosenpassEnabled\n\tmetricsProperties[\"local_users_count\"] = localUsers\n\tmetricsProperties[\"idp_users_count\"] = idpUsers\n\tmetricsProperties[\"embedded_idp_count\"] = len(embeddedIdpTypes)\n\n\tmetricsProperties[\"services\"] = services\n\tmetricsProperties[\"services_enabled\"] = servicesEnabled\n\tmetricsProperties[\"services_targets\"] = servicesTargets\n\tmetricsProperties[\"services_status_active\"] = servicesStatusActive\n\tmetricsProperties[\"services_status_pending\"] = servicesStatusPending\n\tmetricsProperties[\"services_status_error\"] = servicesStatusError\n\tmetricsProperties[\"services_auth_password\"] = servicesAuthPassword\n\tmetricsProperties[\"services_auth_pin\"] = servicesAuthPin\n\tmetricsProperties[\"services_auth_oidc\"] = servicesAuthOIDC\n\tmetricsProperties[\"custom_domains\"] = customDomains\n\tmetricsProperties[\"custom_domains_validated\"] = customDomainsValidated\n\n\tfor targetType, count := range servicesTargetType {\n\t\tmetricsProperties[\"services_target_type_\"+targetType] = count\n\t}\n\n\tfor idpType, count := range embeddedIdpTypes {\n\t\tmetricsProperties[\"embedded_idp_users_\"+idpType] = count\n\t}\n\n\tfor protocol, count := range rulesProtocol {\n\t\tmetricsProperties[\"rules_protocol_\"+protocol] = count\n\t}\n\n\tfor direction, count := range rulesDirection {\n\t\tmetricsProperties[\"rules_direction_\"+direction] = count\n\t}\n\n\tfor os, count := range osPeers {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tfor os, count := range osUIClients {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tmetricsProperties[\"metric_generation_time\"] = time.Since(start).Milliseconds()\n\n\treturn metricsProperties\n}\n\nfunc getAPIKey(ctx context.Context) (string, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\tdefer cancel()\n\n\thttpClient := http.Client{}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+\"/GetToken\", nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create request for metrics public api token %v\", err)\n\t}\n\tresponse, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to request metrics public api token %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing metrics token response body: %v\", err)\n\t\t}\n\t}()\n\n\tif response.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"unable to retrieve metrics token, statusCode %d\", response.StatusCode)\n\t}\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't get metrics token response; %v\", err)\n\t}\n\n\tvar tokenResponse getTokenResponse\n\n\terr = json.Unmarshal(body, &tokenResponse)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't parse metrics public api token; %v\", err)\n\t}\n\n\treturn tokenResponse.PublicAPIToken, nil\n}\n\nfunc buildMetricsPayload(payload pushPayload) (string, error) {\n\tstr, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to marshal metrics payload, got err: %v\", err)\n\t}\n\treturn string(str), nil\n}\n\nfunc createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, context.CancelFunc, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\treqURL := endpoint\n\n\tpayload := strings.NewReader(payloadStr)\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", reqURL, payload)\n\tif err != nil {\n\t\tcancel()\n\t\treturn nil, nil, err\n\t}\n\treq.Header.Add(\"content-type\", \"application/json\")\n\n\treturn req, cancel, nil\n}\n\n// extractIdpType extracts the IdP type from a Dex connector ID.\n// Connector IDs are formatted as \" - \" (e.g., \"okta-abc123\", \"zitadel-xyz\").\n// Returns the type prefix, or \"oidc\" if no known prefix is found.\nfunc extractIdpType(connectorID string) string {\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn strings.ToLower(connectorID)\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}\n\nfunc getMinMaxVersion(inputList []string) (string, string) {\n\tversions := make([]*version.Version, 0)\n\n\tfor _, raw := range inputList {\n\t\tif raw != \"\" && nbversion.SemverRegexp.MatchString(raw) {\n\t\t\tv, err := version.NewVersion(raw)\n\t\t\tif err == nil {\n\t\t\t\tversions = append(versions, v)\n\t\t\t}\n\t\t}\n\t}\n\n\ttargetIndex := 1\n\tl := len(versions)\n\n\tswitch l {\n\tcase 0:\n\t\treturn \"\", \"\"\n\tcase targetIndex:\n\t\tv := versions[targetIndex-1].String()\n\t\treturn v, v\n\tdefault:\n\t\tsort.Sort(version.Collection(versions))\n\t\treturn versions[targetIndex-1].String(), versions[l-1].String()\n\t}\n}","newString":"// Package metrics gather anonymous information about the usage of NetBird management\npackage metrics\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\tnbversion \"github.com/netbirdio/netbird/version\"\n)\n\nconst (\n\t// PayloadEvent identifies an event type\n\tPayloadEvent = \"self-hosted stats\"\n\t// payloadEndpoint metrics defaultEndpoint to send anonymous data\n\tpayloadEndpoint = \"https://metrics.netbird.io\"\n\t// defaultPushInterval default interval to push metrics\n\tdefaultPushInterval = 12 * time.Hour\n\t// requestTimeout http request timeout\n\trequestTimeout = 45 * time.Second\n\tEmbeddedType = \"embedded\"\n)\n\ntype getTokenResponse struct {\n\tPublicAPIToken string `json:\"public_api_token\"`\n}\n\ntype pushPayload struct {\n\tAPIKey string `json:\"api_key\"`\n\tDistinctID string `json:\"distinct_id\"`\n\tEvent string `json:\"event\"`\n\tProperties properties `json:\"properties\"`\n\tTimestamp time.Time `json:\"timestamp\"`\n}\n\n// properties metrics to push\ntype properties map[string]interface{}\n\n// DataSource metric data source\ntype DataSource interface {\n\tGetAllAccounts(ctx context.Context) []*types.Account\n\tGetStoreEngine() types.Engine\n\tGetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)\n}\n\n// ConnManager peer connection manager that holds state for current active connections\ntype ConnManager interface {\n\tGetAllConnectedPeers() map[string]struct{}\n}\n\n// Worker metrics collector and pusher\ntype Worker struct {\n\tctx context.Context\n\tid string\n\tidpManager string\n\tdataSource DataSource\n\tconnManager ConnManager\n\tstartupTime time.Time\n\tlastRun time.Time\n}\n\n// NewWorker returns a metrics worker\nfunc NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {\n\tcurrentTime := time.Now()\n\treturn &Worker{\n\t\tctx: ctx,\n\t\tid: id,\n\t\tidpManager: idpManager,\n\t\tdataSource: dataSource,\n\t\tconnManager: connManager,\n\t\tstartupTime: currentTime,\n\t\tlastRun: currentTime,\n\t}\n}\n\n// Run runs the metrics worker\nfunc (w *Worker) Run(ctx context.Context) {\n\tinterval := getMetricsInterval(ctx)\n\n\tpushTicker := time.NewTicker(interval)\n\tfor {\n\t\tselect {\n\t\tcase <-w.ctx.Done():\n\t\t\treturn\n\t\tcase <-pushTicker.C:\n\t\t\terr := w.sendMetrics(ctx)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithContext(ctx).Error(err)\n\t\t\t}\n\t\t\tw.lastRun = time.Now()\n\t\t}\n\t}\n}\n\nfunc getMetricsInterval(ctx context.Context) time.Duration {\n\tinterval := defaultPushInterval\n\tif os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") != \"\" {\n\t\tnewInterval, err := time.ParseDuration(os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") + \"s\")\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"unable to parse NETBIRD_METRICS_INTERVAL_IN_SECONDS, using default interval %v. Error: %v\", defaultPushInterval, err)\n\t\t} else {\n\t\t\tlog.WithContext(ctx).Infof(\"using NETBIRD_METRICS_INTERVAL_IN_SECONDS %s\", newInterval)\n\t\t\tinterval = newInterval\n\t\t}\n\t}\n\treturn interval\n}\n\nfunc (w *Worker) sendMetrics(ctx context.Context) error {\n\tapiKey, err := getAPIKey(w.ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpayload := w.generatePayload(ctx, apiKey)\n\n\tpayloadString, err := buildMetricsPayload(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thttpClient := http.Client{}\n\n\texportJobReq, cancelCTX, err := createPostRequest(w.ctx, payloadEndpoint+\"/capture/\", payloadString)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create metrics post request %v\", err)\n\t}\n\tdefer cancelCTX()\n\n\tjobResp, err := httpClient.Do(exportJobReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to push metrics %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = jobResp.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing update metrics response body: %v\", err)\n\t\t}\n\t}()\n\n\tif jobResp.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"unable to push anonymous metrics, got statusCode %d\", jobResp.StatusCode)\n\t}\n\n\tlog.WithContext(ctx).Infof(\"sent anonymous metrics, next push will happen in %s. \"+\n\t\t\"You can disable these metrics by running with flag --disable-anonymous-metrics,\"+\n\t\t\" see more information at https://docs.netbird.io/about-netbird/faq#why-and-what-are-the-anonymous-usage-metrics\", getMetricsInterval(ctx))\n\n\treturn nil\n}\n\nfunc (w *Worker) generatePayload(ctx context.Context, apiKey string) pushPayload {\n\tproperties := w.generateProperties(ctx)\n\n\treturn pushPayload{\n\t\tAPIKey: apiKey,\n\t\tDistinctID: w.id,\n\t\tEvent: PayloadEvent,\n\t\tProperties: properties,\n\t\tTimestamp: time.Now(),\n\t}\n}\n\nfunc (w *Worker) generateProperties(ctx context.Context) properties {\n\tvar (\n\t\tuptime float64\n\t\taccounts int\n\t\texpirationEnabled int\n\t\tusers int\n\t\tserviceUsers int\n\t\tpats int\n\t\tpeers int\n\t\tpeersSSHEnabled int\n\t\tsetupKeysUsage int\n\t\tephemeralPeersSKs int\n\t\tephemeralPeersSKUsage int\n\t\tactivePeersLastDay int\n\t\tactiveUserPeersLastDay int\n\t\tosPeers map[string]int\n\t\tactiveUsersLastDay map[string]struct{}\n\t\tuserPeers int\n\t\trules int\n\t\trulesProtocol map[string]int\n\t\trulesDirection map[string]int\n\t\trulesWithSrcPostureChecks int\n\t\tpostureChecks int\n\t\tgroups int\n\t\troutes int\n\t\troutesWithRGGroups int\n\t\tnetworks int\n\t\tnetworkResources int\n\t\tnetworkRouters int\n\t\tnetworkRoutersWithPG int\n\t\tnameservers int\n\t\tuiClient int\n\t\tversion string\n\t\tpeerActiveVersions []string\n\t\tosUIClients map[string]int\n\t\trosenpassEnabled int\n\t\tlocalUsers int\n\t\tidpUsers int\n\t\tembeddedIdpTypes map[string]int\n\t\tservices int\n\t\tservicesEnabled int\n\t\tservicesTargets int\n\t\tservicesStatusActive int\n\t\tservicesStatusPending int\n\t\tservicesStatusError int\n\t\tservicesTargetType map[string]int\n\t\tservicesAuthPassword int\n\t\tservicesAuthPin int\n\t\tservicesAuthOIDC int\n\t)\n\tstart := time.Now()\n\tmetricsProperties := make(properties)\n\tosPeers = make(map[string]int)\n\tosUIClients = make(map[string]int)\n\trulesProtocol = make(map[string]int)\n\trulesDirection = make(map[string]int)\n\tactiveUsersLastDay = make(map[string]struct{})\n\tembeddedIdpTypes = make(map[string]int)\n\tservicesTargetType = make(map[string]int)\n\tuptime = time.Since(w.startupTime).Seconds()\n\tconnections := w.connManager.GetAllConnectedPeers()\n\tversion = nbversion.NetbirdVersion()\n\n\tcustomDomains, customDomainsValidated, _ := w.dataSource.GetCustomDomainsCounts(ctx)\n\n\tfor _, account := range w.dataSource.GetAllAccounts(ctx) {\n\t\taccounts++\n\n\t\tif account.Settings.PeerLoginExpirationEnabled {\n\t\t\texpirationEnabled++\n\t\t}\n\n\t\tgroups += len(account.Groups)\n\t\tnetworks += len(account.Networks)\n\t\tnetworkResources += len(account.NetworkResources)\n\n\t\tnetworkRouters += len(account.NetworkRouters)\n\t\tfor _, router := range account.NetworkRouters {\n\t\t\tif len(router.PeerGroups) > 0 {\n\t\t\t\tnetworkRoutersWithPG++\n\t\t\t}\n\t\t}\n\n\t\troutes += len(account.Routes)\n\t\tfor _, route := range account.Routes {\n\t\t\tif len(route.PeerGroups) > 0 {\n\t\t\t\troutesWithRGGroups++\n\t\t\t}\n\t\t}\n\t\tnameservers += len(account.NameServerGroups)\n\n\t\tfor _, policy := range account.Policies {\n\t\t\tfor _, rule := range policy.Rules {\n\t\t\t\trules++\n\t\t\t\trulesProtocol[string(rule.Protocol)]++\n\t\t\t\tif rule.Bidirectional {\n\t\t\t\t\trulesDirection[\"bidirectional\"]++\n\t\t\t\t} else {\n\t\t\t\t\trulesDirection[\"oneway\"]++\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(policy.SourcePostureChecks) > 0 {\n\t\t\t\trulesWithSrcPostureChecks++\n\t\t\t}\n\t\t}\n\n\t\tpostureChecks += len(account.PostureChecks)\n\n\t\tfor _, user := range account.Users {\n\t\t\tif user.IsServiceUser {\n\t\t\t\tserviceUsers++\n\t\t\t} else {\n\t\t\t\tusers++\n\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t}\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpats += len(user.PATs)\n\t\t}\n\n\t\tfor _, key := range account.SetupKeys {\n\t\t\tsetupKeysUsage += key.UsedTimes\n\t\t\tif key.Ephemeral {\n\t\t\t\tephemeralPeersSKs++\n\t\t\t\tephemeralPeersSKUsage += key.UsedTimes\n\t\t\t}\n\t\t}\n\n\t\tfor _, peer := range account.Peers {\n\t\t\tpeers++\n\n\t\t\tif peer.SSHEnabled || peer.Meta.Flags.ServerSSHAllowed {\n\t\t\t\tpeersSSHEnabled++\n\t\t\t}\n\n\t\t\tif peer.Meta.Flags.RosenpassEnabled {\n\t\t\t\trosenpassEnabled++\n\t\t\t}\n\n\t\t\tif peer.UserID != \"\" {\n\t\t\t\tuserPeers++\n\t\t\t}\n\n\t\t\tosKey := strings.ToLower(fmt.Sprintf(\"peer_os_%s\", peer.Meta.GoOS))\n\t\t\tosCount := osPeers[osKey]\n\t\t\tosPeers[osKey] = osCount + 1\n\n\t\t\tif peer.Meta.UIVersion != \"\" {\n\t\t\t\tuiClient++\n\t\t\t\tuiOSKey := strings.ToLower(fmt.Sprintf(\"ui_client_os_%s\", peer.Meta.GoOS))\n\t\t\t\tosUICount := osUIClients[uiOSKey]\n\t\t\t\tosUIClients[uiOSKey] = osUICount + 1\n\t\t\t}\n\n\t\t\t_, connected := connections[peer.ID]\n\t\t\tif connected || peer.Status.LastSeen.After(w.lastRun) {\n\t\t\t\tactivePeersLastDay++\n\t\t\t\tif peer.UserID != \"\" {\n\t\t\t\t\tactiveUserPeersLastDay++\n\t\t\t\t\tactiveUsersLastDay[peer.UserID] = struct{}{}\n\t\t\t\t}\n\t\t\t\tosActiveKey := osKey + \"_active\"\n\t\t\t\tosActiveCount := osPeers[osActiveKey]\n\t\t\t\tosPeers[osActiveKey] = osActiveCount + 1\n\t\t\t\tpeerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)\n\t\t\t}\n\t\t}\n\n\t\tfor _, service := range account.Services {\n\t\t\tservices++\n\t\t\tif service.Enabled {\n\t\t\t\tservicesEnabled++\n\t\t\t}\n\t\t\tservicesTargets += len(service.Targets)\n\n\t\t\tswitch rpservice.Status(service.Meta.Status) {\n\t\t\tcase rpservice.StatusActive:\n\t\t\t\tservicesStatusActive++\n\t\t\tcase rpservice.StatusPending:\n\t\t\t\tservicesStatusPending++\n\t\t\tcase rpservice.StatusError, rpservice.StatusCertificateFailed, rpservice.StatusTunnelNotCreated:\n\t\t\t\tservicesStatusError++\n\t\t\t}\n\n\t\t\tfor _, target := range service.Targets {\n\t\t\t\tservicesTargetType[target.TargetType]++\n\t\t\t}\n\n\t\t\tif service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled {\n\t\t\t\tservicesAuthPassword++\n\t\t\t}\n\t\t\tif service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled {\n\t\t\t\tservicesAuthPin++\n\t\t\t}\n\t\t\tif service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled {\n\t\t\t\tservicesAuthOIDC++\n\t\t\t}\n\t\t}\n\t}\n\n\tminActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)\n\tmetricsProperties[\"uptime\"] = uptime\n\tmetricsProperties[\"accounts\"] = accounts\n\tmetricsProperties[\"users\"] = users\n\tmetricsProperties[\"service_users\"] = serviceUsers\n\tmetricsProperties[\"pats\"] = pats\n\tmetricsProperties[\"peers\"] = peers\n\tmetricsProperties[\"peers_ssh_enabled\"] = peersSSHEnabled\n\tmetricsProperties[\"peers_login_expiration_enabled\"] = expirationEnabled\n\tmetricsProperties[\"setup_keys_usage\"] = setupKeysUsage\n\tmetricsProperties[\"ephemeral_peers_setup_keys\"] = ephemeralPeersSKs\n\tmetricsProperties[\"ephemeral_peers_setup_keys_usage\"] = ephemeralPeersSKUsage\n\tmetricsProperties[\"active_peers_last_day\"] = activePeersLastDay\n\tmetricsProperties[\"active_user_peers_last_day\"] = activeUserPeersLastDay\n\tmetricsProperties[\"active_users_last_day\"] = len(activeUsersLastDay)\n\tmetricsProperties[\"user_peers\"] = userPeers\n\tmetricsProperties[\"rules\"] = rules\n\tmetricsProperties[\"rules_with_src_posture_checks\"] = rulesWithSrcPostureChecks\n\tmetricsProperties[\"posture_checks\"] = postureChecks\n\tmetricsProperties[\"groups\"] = groups\n\tmetricsProperties[\"networks\"] = networks\n\tmetricsProperties[\"network_resources\"] = networkResources\n\tmetricsProperties[\"network_routers\"] = networkRouters\n\tmetricsProperties[\"network_routers_with_groups\"] = networkRoutersWithPG\n\tmetricsProperties[\"routes\"] = routes\n\tmetricsProperties[\"routes_with_routing_groups\"] = routesWithRGGroups\n\tmetricsProperties[\"nameservers\"] = nameservers\n\tmetricsProperties[\"version\"] = version\n\tmetricsProperties[\"min_active_peer_version\"] = minActivePeerVersion\n\tmetricsProperties[\"max_active_peer_version\"] = maxActivePeerVersion\n\tmetricsProperties[\"ui_clients\"] = uiClient\n\tmetricsProperties[\"idp_manager\"] = w.idpManager\n\tmetricsProperties[\"store_engine\"] = w.dataSource.GetStoreEngine()\n\tmetricsProperties[\"rosenpass_enabled\"] = rosenpassEnabled\n\tmetricsProperties[\"local_users_count\"] = localUsers\n\tmetricsProperties[\"idp_users_count\"] = idpUsers\n\tmetricsProperties[\"embedded_idp_count\"] = len(embeddedIdpTypes)\n\n\tmetricsProperties[\"services\"] = services\n\tmetricsProperties[\"services_enabled\"] = servicesEnabled\n\tmetricsProperties[\"services_targets\"] = servicesTargets\n\tmetricsProperties[\"services_status_active\"] = servicesStatusActive\n\tmetricsProperties[\"services_status_pending\"] = servicesStatusPending\n\tmetricsProperties[\"services_status_error\"] = servicesStatusError\n\tmetricsProperties[\"services_auth_password\"] = servicesAuthPassword\n\tmetricsProperties[\"services_auth_pin\"] = servicesAuthPin\n\tmetricsProperties[\"services_auth_oidc\"] = servicesAuthOIDC\n\tmetricsProperties[\"custom_domains\"] = customDomains\n\tmetricsProperties[\"custom_domains_validated\"] = customDomainsValidated\n\n\tfor targetType, count := range servicesTargetType {\n\t\tmetricsProperties[\"services_target_type_\"+targetType] = count\n\t}\n\n\tfor idpType, count := range embeddedIdpTypes {\n\t\tmetricsProperties[\"embedded_idp_users_\"+idpType] = count\n\t}\n\n\tfor protocol, count := range rulesProtocol {\n\t\tmetricsProperties[\"rules_protocol_\"+protocol] = count\n\t}\n\n\tfor direction, count := range rulesDirection {\n\t\tmetricsProperties[\"rules_direction_\"+direction] = count\n\t}\n\n\tfor os, count := range osPeers {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tfor os, count := range osUIClients {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tmetricsProperties[\"metric_generation_time\"] = time.Since(start).Milliseconds()\n\n\treturn metricsProperties\n}\n\nfunc getAPIKey(ctx context.Context) (string, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\tdefer cancel()\n\n\thttpClient := http.Client{}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+\"/GetToken\", nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create request for metrics public api token %v\", err)\n\t}\n\tresponse, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to request metrics public api token %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing metrics token response body: %v\", err)\n\t\t}\n\t}()\n\n\tif response.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"unable to retrieve metrics token, statusCode %d\", response.StatusCode)\n\t}\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't get metrics token response; %v\", err)\n\t}\n\n\tvar tokenResponse getTokenResponse\n\n\terr = json.Unmarshal(body, &tokenResponse)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't parse metrics public api token; %v\", err)\n\t}\n\n\treturn tokenResponse.PublicAPIToken, nil\n}\n\nfunc buildMetricsPayload(payload pushPayload) (string, error) {\n\tstr, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to marshal metrics payload, got err: %v\", err)\n\t}\n\treturn string(str), nil\n}\n\nfunc createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, context.CancelFunc, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\treqURL := endpoint\n\n\tpayload := strings.NewReader(payloadStr)\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", reqURL, payload)\n\tif err != nil {\n\t\tcancel()\n\t\treturn nil, nil, err\n\t}\n\treq.Header.Add(\"content-type\", \"application/json\")\n\n\treturn req, cancel, nil\n}\n\n// extractIdpType extracts the IdP type from a Dex connector ID.\n// Connector IDs are formatted as \" - \" (e.g., \"okta-abc123\", \"zitadel-xyz\").\n// Returns the type prefix, or \"oidc\" if no known prefix is found.\nfunc extractIdpType(connectorID string) string {\n\tif connectorID == \"\" {\n\t\treturn \"unknown\"\n\t}\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn strings.ToLower(connectorID)\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}\n\nfunc getMinMaxVersion(inputList []string) (string, string) {\n\tversions := make([]*version.Version, 0)\n\n\tfor _, raw := range inputList {\n\t\tif raw != \"\" && nbversion.SemverRegexp.MatchString(raw) {\n\t\t\tv, err := version.NewVersion(raw)\n\t\t\tif err == nil {\n\t\t\t\tversions = append(versions, v)\n\t\t\t}\n\t\t}\n\t}\n\n\ttargetIndex := 1\n\tl := len(versions)\n\n\tswitch l {\n\tcase 0:\n\t\treturn \"\", \"\"\n\tcase targetIndex:\n\t\tv := versions[targetIndex-1].String()\n\t\treturn v, v\n\tdefault:\n\t\tsort.Sort(version.Collection(versions))\n\t\treturn versions[targetIndex-1].String(), versions[l-1].String()\n\t}\n}","originalFile":"// Package metrics gather anonymous information about the usage of NetBird management\npackage metrics\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\tnbversion \"github.com/netbirdio/netbird/version\"\n)\n\nconst (\n\t// PayloadEvent identifies an event type\n\tPayloadEvent = \"self-hosted stats\"\n\t// payloadEndpoint metrics defaultEndpoint to send anonymous data\n\tpayloadEndpoint = \"https://metrics.netbird.io\"\n\t// defaultPushInterval default interval to push metrics\n\tdefaultPushInterval = 12 * time.Hour\n\t// requestTimeout http request timeout\n\trequestTimeout = 45 * time.Second\n\tEmbeddedType = \"embedded\"\n)\n\ntype getTokenResponse struct {\n\tPublicAPIToken string `json:\"public_api_token\"`\n}\n\ntype pushPayload struct {\n\tAPIKey string `json:\"api_key\"`\n\tDistinctID string `json:\"distinct_id\"`\n\tEvent string `json:\"event\"`\n\tProperties properties `json:\"properties\"`\n\tTimestamp time.Time `json:\"timestamp\"`\n}\n\n// properties metrics to push\ntype properties map[string]interface{}\n\n// DataSource metric data source\ntype DataSource interface {\n\tGetAllAccounts(ctx context.Context) []*types.Account\n\tGetStoreEngine() types.Engine\n\tGetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)\n}\n\n// ConnManager peer connection manager that holds state for current active connections\ntype ConnManager interface {\n\tGetAllConnectedPeers() map[string]struct{}\n}\n\n// Worker metrics collector and pusher\ntype Worker struct {\n\tctx context.Context\n\tid string\n\tidpManager string\n\tdataSource DataSource\n\tconnManager ConnManager\n\tstartupTime time.Time\n\tlastRun time.Time\n}\n\n// NewWorker returns a metrics worker\nfunc NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {\n\tcurrentTime := time.Now()\n\treturn &Worker{\n\t\tctx: ctx,\n\t\tid: id,\n\t\tidpManager: idpManager,\n\t\tdataSource: dataSource,\n\t\tconnManager: connManager,\n\t\tstartupTime: currentTime,\n\t\tlastRun: currentTime,\n\t}\n}\n\n// Run runs the metrics worker\nfunc (w *Worker) Run(ctx context.Context) {\n\tinterval := getMetricsInterval(ctx)\n\n\tpushTicker := time.NewTicker(interval)\n\tfor {\n\t\tselect {\n\t\tcase <-w.ctx.Done():\n\t\t\treturn\n\t\tcase <-pushTicker.C:\n\t\t\terr := w.sendMetrics(ctx)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithContext(ctx).Error(err)\n\t\t\t}\n\t\t\tw.lastRun = time.Now()\n\t\t}\n\t}\n}\n\nfunc getMetricsInterval(ctx context.Context) time.Duration {\n\tinterval := defaultPushInterval\n\tif os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") != \"\" {\n\t\tnewInterval, err := time.ParseDuration(os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") + \"s\")\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"unable to parse NETBIRD_METRICS_INTERVAL_IN_SECONDS, using default interval %v. Error: %v\", defaultPushInterval, err)\n\t\t} else {\n\t\t\tlog.WithContext(ctx).Infof(\"using NETBIRD_METRICS_INTERVAL_IN_SECONDS %s\", newInterval)\n\t\t\tinterval = newInterval\n\t\t}\n\t}\n\treturn interval\n}\n\nfunc (w *Worker) sendMetrics(ctx context.Context) error {\n\tapiKey, err := getAPIKey(w.ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpayload := w.generatePayload(ctx, apiKey)\n\n\tpayloadString, err := buildMetricsPayload(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thttpClient := http.Client{}\n\n\texportJobReq, cancelCTX, err := createPostRequest(w.ctx, payloadEndpoint+\"/capture/\", payloadString)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create metrics post request %v\", err)\n\t}\n\tdefer cancelCTX()\n\n\tjobResp, err := httpClient.Do(exportJobReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to push metrics %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = jobResp.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing update metrics response body: %v\", err)\n\t\t}\n\t}()\n\n\tif jobResp.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"unable to push anonymous metrics, got statusCode %d\", jobResp.StatusCode)\n\t}\n\n\tlog.WithContext(ctx).Infof(\"sent anonymous metrics, next push will happen in %s. \"+\n\t\t\"You can disable these metrics by running with flag --disable-anonymous-metrics,\"+\n\t\t\" see more information at https://docs.netbird.io/about-netbird/faq#why-and-what-are-the-anonymous-usage-metrics\", getMetricsInterval(ctx))\n\n\treturn nil\n}\n\nfunc (w *Worker) generatePayload(ctx context.Context, apiKey string) pushPayload {\n\tproperties := w.generateProperties(ctx)\n\n\treturn pushPayload{\n\t\tAPIKey: apiKey,\n\t\tDistinctID: w.id,\n\t\tEvent: PayloadEvent,\n\t\tProperties: properties,\n\t\tTimestamp: time.Now(),\n\t}\n}\n\nfunc (w *Worker) generateProperties(ctx context.Context) properties {\n\tvar (\n\t\tuptime float64\n\t\taccounts int\n\t\texpirationEnabled int\n\t\tusers int\n\t\tserviceUsers int\n\t\tpats int\n\t\tpeers int\n\t\tpeersSSHEnabled int\n\t\tsetupKeysUsage int\n\t\tephemeralPeersSKs int\n\t\tephemeralPeersSKUsage int\n\t\tactivePeersLastDay int\n\t\tactiveUserPeersLastDay int\n\t\tosPeers map[string]int\n\t\tactiveUsersLastDay map[string]struct{}\n\t\tuserPeers int\n\t\trules int\n\t\trulesProtocol map[string]int\n\t\trulesDirection map[string]int\n\t\trulesWithSrcPostureChecks int\n\t\tpostureChecks int\n\t\tgroups int\n\t\troutes int\n\t\troutesWithRGGroups int\n\t\tnetworks int\n\t\tnetworkResources int\n\t\tnetworkRouters int\n\t\tnetworkRoutersWithPG int\n\t\tnameservers int\n\t\tuiClient int\n\t\tversion string\n\t\tpeerActiveVersions []string\n\t\tosUIClients map[string]int\n\t\trosenpassEnabled int\n\t\tlocalUsers int\n\t\tidpUsers int\n\t\tembeddedIdpTypes map[string]int\n\t\tservices int\n\t\tservicesEnabled int\n\t\tservicesTargets int\n\t\tservicesStatusActive int\n\t\tservicesStatusPending int\n\t\tservicesStatusError int\n\t\tservicesTargetType map[string]int\n\t\tservicesAuthPassword int\n\t\tservicesAuthPin int\n\t\tservicesAuthOIDC int\n\t)\n\tstart := time.Now()\n\tmetricsProperties := make(properties)\n\tosPeers = make(map[string]int)\n\tosUIClients = make(map[string]int)\n\trulesProtocol = make(map[string]int)\n\trulesDirection = make(map[string]int)\n\tactiveUsersLastDay = make(map[string]struct{})\n\tembeddedIdpTypes = make(map[string]int)\n\tservicesTargetType = make(map[string]int)\n\tuptime = time.Since(w.startupTime).Seconds()\n\tconnections := w.connManager.GetAllConnectedPeers()\n\tversion = nbversion.NetbirdVersion()\n\n\tcustomDomains, customDomainsValidated, _ := w.dataSource.GetCustomDomainsCounts(ctx)\n\n\tfor _, account := range w.dataSource.GetAllAccounts(ctx) {\n\t\taccounts++\n\n\t\tif account.Settings.PeerLoginExpirationEnabled {\n\t\t\texpirationEnabled++\n\t\t}\n\n\t\tgroups += len(account.Groups)\n\t\tnetworks += len(account.Networks)\n\t\tnetworkResources += len(account.NetworkResources)\n\n\t\tnetworkRouters += len(account.NetworkRouters)\n\t\tfor _, router := range account.NetworkRouters {\n\t\t\tif len(router.PeerGroups) > 0 {\n\t\t\t\tnetworkRoutersWithPG++\n\t\t\t}\n\t\t}\n\n\t\troutes += len(account.Routes)\n\t\tfor _, route := range account.Routes {\n\t\t\tif len(route.PeerGroups) > 0 {\n\t\t\t\troutesWithRGGroups++\n\t\t\t}\n\t\t}\n\t\tnameservers += len(account.NameServerGroups)\n\n\t\tfor _, policy := range account.Policies {\n\t\t\tfor _, rule := range policy.Rules {\n\t\t\t\trules++\n\t\t\t\trulesProtocol[string(rule.Protocol)]++\n\t\t\t\tif rule.Bidirectional {\n\t\t\t\t\trulesDirection[\"bidirectional\"]++\n\t\t\t\t} else {\n\t\t\t\t\trulesDirection[\"oneway\"]++\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(policy.SourcePostureChecks) > 0 {\n\t\t\t\trulesWithSrcPostureChecks++\n\t\t\t}\n\t\t}\n\n\t\tpostureChecks += len(account.PostureChecks)\n\n\t\tfor _, user := range account.Users {\n\t\t\tif user.IsServiceUser {\n\t\t\t\tserviceUsers++\n\t\t\t} else {\n\t\t\t\tusers++\n\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t}\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpats += len(user.PATs)\n\t\t}\n\n\t\tfor _, key := range account.SetupKeys {\n\t\t\tsetupKeysUsage += key.UsedTimes\n\t\t\tif key.Ephemeral {\n\t\t\t\tephemeralPeersSKs++\n\t\t\t\tephemeralPeersSKUsage += key.UsedTimes\n\t\t\t}\n\t\t}\n\n\t\tfor _, peer := range account.Peers {\n\t\t\tpeers++\n\n\t\t\tif peer.SSHEnabled || peer.Meta.Flags.ServerSSHAllowed {\n\t\t\t\tpeersSSHEnabled++\n\t\t\t}\n\n\t\t\tif peer.Meta.Flags.RosenpassEnabled {\n\t\t\t\trosenpassEnabled++\n\t\t\t}\n\n\t\t\tif peer.UserID != \"\" {\n\t\t\t\tuserPeers++\n\t\t\t}\n\n\t\t\tosKey := strings.ToLower(fmt.Sprintf(\"peer_os_%s\", peer.Meta.GoOS))\n\t\t\tosCount := osPeers[osKey]\n\t\t\tosPeers[osKey] = osCount + 1\n\n\t\t\tif peer.Meta.UIVersion != \"\" {\n\t\t\t\tuiClient++\n\t\t\t\tuiOSKey := strings.ToLower(fmt.Sprintf(\"ui_client_os_%s\", peer.Meta.GoOS))\n\t\t\t\tosUICount := osUIClients[uiOSKey]\n\t\t\t\tosUIClients[uiOSKey] = osUICount + 1\n\t\t\t}\n\n\t\t\t_, connected := connections[peer.ID]\n\t\t\tif connected || peer.Status.LastSeen.After(w.lastRun) {\n\t\t\t\tactivePeersLastDay++\n\t\t\t\tif peer.UserID != \"\" {\n\t\t\t\t\tactiveUserPeersLastDay++\n\t\t\t\t\tactiveUsersLastDay[peer.UserID] = struct{}{}\n\t\t\t\t}\n\t\t\t\tosActiveKey := osKey + \"_active\"\n\t\t\t\tosActiveCount := osPeers[osActiveKey]\n\t\t\t\tosPeers[osActiveKey] = osActiveCount + 1\n\t\t\t\tpeerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)\n\t\t\t}\n\t\t}\n\n\t\tfor _, service := range account.Services {\n\t\t\tservices++\n\t\t\tif service.Enabled {\n\t\t\t\tservicesEnabled++\n\t\t\t}\n\t\t\tservicesTargets += len(service.Targets)\n\n\t\t\tswitch rpservice.Status(service.Meta.Status) {\n\t\t\tcase rpservice.StatusActive:\n\t\t\t\tservicesStatusActive++\n\t\t\tcase rpservice.StatusPending:\n\t\t\t\tservicesStatusPending++\n\t\t\tcase rpservice.StatusError, rpservice.StatusCertificateFailed, rpservice.StatusTunnelNotCreated:\n\t\t\t\tservicesStatusError++\n\t\t\t}\n\n\t\t\tfor _, target := range service.Targets {\n\t\t\t\tservicesTargetType[target.TargetType]++\n\t\t\t}\n\n\t\t\tif service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled {\n\t\t\t\tservicesAuthPassword++\n\t\t\t}\n\t\t\tif service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled {\n\t\t\t\tservicesAuthPin++\n\t\t\t}\n\t\t\tif service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled {\n\t\t\t\tservicesAuthOIDC++\n\t\t\t}\n\t\t}\n\t}\n\n\tminActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)\n\tmetricsProperties[\"uptime\"] = uptime\n\tmetricsProperties[\"accounts\"] = accounts\n\tmetricsProperties[\"users\"] = users\n\tmetricsProperties[\"service_users\"] = serviceUsers\n\tmetricsProperties[\"pats\"] = pats\n\tmetricsProperties[\"peers\"] = peers\n\tmetricsProperties[\"peers_ssh_enabled\"] = peersSSHEnabled\n\tmetricsProperties[\"peers_login_expiration_enabled\"] = expirationEnabled\n\tmetricsProperties[\"setup_keys_usage\"] = setupKeysUsage\n\tmetricsProperties[\"ephemeral_peers_setup_keys\"] = ephemeralPeersSKs\n\tmetricsProperties[\"ephemeral_peers_setup_keys_usage\"] = ephemeralPeersSKUsage\n\tmetricsProperties[\"active_peers_last_day\"] = activePeersLastDay\n\tmetricsProperties[\"active_user_peers_last_day\"] = activeUserPeersLastDay\n\tmetricsProperties[\"active_users_last_day\"] = len(activeUsersLastDay)\n\tmetricsProperties[\"user_peers\"] = userPeers\n\tmetricsProperties[\"rules\"] = rules\n\tmetricsProperties[\"rules_with_src_posture_checks\"] = rulesWithSrcPostureChecks\n\tmetricsProperties[\"posture_checks\"] = postureChecks\n\tmetricsProperties[\"groups\"] = groups\n\tmetricsProperties[\"networks\"] = networks\n\tmetricsProperties[\"network_resources\"] = networkResources\n\tmetricsProperties[\"network_routers\"] = networkRouters\n\tmetricsProperties[\"network_routers_with_groups\"] = networkRoutersWithPG\n\tmetricsProperties[\"routes\"] = routes\n\tmetricsProperties[\"routes_with_routing_groups\"] = routesWithRGGroups\n\tmetricsProperties[\"nameservers\"] = nameservers\n\tmetricsProperties[\"version\"] = version\n\tmetricsProperties[\"min_active_peer_version\"] = minActivePeerVersion\n\tmetricsProperties[\"max_active_peer_version\"] = maxActivePeerVersion\n\tmetricsProperties[\"ui_clients\"] = uiClient\n\tmetricsProperties[\"idp_manager\"] = w.idpManager\n\tmetricsProperties[\"store_engine\"] = w.dataSource.GetStoreEngine()\n\tmetricsProperties[\"rosenpass_enabled\"] = rosenpassEnabled\n\tmetricsProperties[\"local_users_count\"] = localUsers\n\tmetricsProperties[\"idp_users_count\"] = idpUsers\n\tmetricsProperties[\"embedded_idp_count\"] = len(embeddedIdpTypes)\n\n\tmetricsProperties[\"services\"] = services\n\tmetricsProperties[\"services_enabled\"] = servicesEnabled\n\tmetricsProperties[\"services_targets\"] = servicesTargets\n\tmetricsProperties[\"services_status_active\"] = servicesStatusActive\n\tmetricsProperties[\"services_status_pending\"] = servicesStatusPending\n\tmetricsProperties[\"services_status_error\"] = servicesStatusError\n\tmetricsProperties[\"services_auth_password\"] = servicesAuthPassword\n\tmetricsProperties[\"services_auth_pin\"] = servicesAuthPin\n\tmetricsProperties[\"services_auth_oidc\"] = servicesAuthOIDC\n\tmetricsProperties[\"custom_domains\"] = customDomains\n\tmetricsProperties[\"custom_domains_validated\"] = customDomainsValidated\n\n\tfor targetType, count := range servicesTargetType {\n\t\tmetricsProperties[\"services_target_type_\"+targetType] = count\n\t}\n\n\tfor idpType, count := range embeddedIdpTypes {\n\t\tmetricsProperties[\"embedded_idp_users_\"+idpType] = count\n\t}\n\n\tfor protocol, count := range rulesProtocol {\n\t\tmetricsProperties[\"rules_protocol_\"+protocol] = count\n\t}\n\n\tfor direction, count := range rulesDirection {\n\t\tmetricsProperties[\"rules_direction_\"+direction] = count\n\t}\n\n\tfor os, count := range osPeers {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tfor os, count := range osUIClients {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tmetricsProperties[\"metric_generation_time\"] = time.Since(start).Milliseconds()\n\n\treturn metricsProperties\n}\n\nfunc getAPIKey(ctx context.Context) (string, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\tdefer cancel()\n\n\thttpClient := http.Client{}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+\"/GetToken\", nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create request for metrics public api token %v\", err)\n\t}\n\tresponse, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to request metrics public api token %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing metrics token response body: %v\", err)\n\t\t}\n\t}()\n\n\tif response.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"unable to retrieve metrics token, statusCode %d\", response.StatusCode)\n\t}\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't get metrics token response; %v\", err)\n\t}\n\n\tvar tokenResponse getTokenResponse\n\n\terr = json.Unmarshal(body, &tokenResponse)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't parse metrics public api token; %v\", err)\n\t}\n\n\treturn tokenResponse.PublicAPIToken, nil\n}\n\nfunc buildMetricsPayload(payload pushPayload) (string, error) {\n\tstr, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to marshal metrics payload, got err: %v\", err)\n\t}\n\treturn string(str), nil\n}\n\nfunc createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, context.CancelFunc, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\treqURL := endpoint\n\n\tpayload := strings.NewReader(payloadStr)\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", reqURL, payload)\n\tif err != nil {\n\t\tcancel()\n\t\treturn nil, nil, err\n\t}\n\treq.Header.Add(\"content-type\", \"application/json\")\n\n\treturn req, cancel, nil\n}\n\n// extractIdpType extracts the IdP type from a Dex connector ID.\n// Connector IDs are formatted as \" - \" (e.g., \"okta-abc123\", \"zitadel-xyz\").\n// Returns the type prefix, or \"oidc\" if no known prefix is found.\nfunc extractIdpType(connectorID string) string {\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn strings.ToLower(connectorID)\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}\n\nfunc getMinMaxVersion(inputList []string) (string, string) {\n\tversions := make([]*version.Version, 0)\n\n\tfor _, raw := range inputList {\n\t\tif raw != \"\" && nbversion.SemverRegexp.MatchString(raw) {\n\t\t\tv, err := version.NewVersion(raw)\n\t\t\tif err == nil {\n\t\t\t\tversions = append(versions, v)\n\t\t\t}\n\t\t}\n\t}\n\n\ttargetIndex := 1\n\tl := len(versions)\n\n\tswitch l {\n\tcase 0:\n\t\treturn \"\", \"\"\n\tcase targetIndex:\n\t\tv := versions[targetIndex-1].String()\n\t\treturn v, v\n\tdefault:\n\t\tsort.Sort(version.Collection(versions))\n\t\treturn versions[targetIndex-1].String(), versions[l-1].String()\n\t}\n}\n","structuredPatch":[{"oldStart":531,"oldLines":6,"newStart":531,"newLines":9,"lines":[" // Connector IDs are formatted as \" - \" (e.g., \"okta-abc123\", \"zitadel-xyz\")."," // Returns the type prefix, or \"oidc\" if no known prefix is found."," func extractIdpType(connectorID string) string {","+ if connectorID == \"\" {","+ return \"unknown\"","+ }"," idx := strings.LastIndex(connectorID, \"-\")"," if idx <= 0 {"," return strings.ToLower(connectorID)"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"e671a191-27fc-4bd0-b031-6a45a570f389"} +{"parentUuid":"5f79abe1-9477-4080-b643-d87b1a1459b5","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01QrD2MQ7NhAyFFJuXCte1qA","type":"message","role":"assistant","content":[{"type":"text","text":"Now update the tests:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":1264,"cache_read_input_tokens":120141,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":1264},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhqMyLh7dEeuVSxHQsVv","type":"assistant","uuid":"f766afc2-20b7-4443-8b4f-c90706da21a3","timestamp":"2026-03-04T09:52:14.382Z"} +{"parentUuid":"f766afc2-20b7-4443-8b4f-c90706da21a3","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01QrD2MQ7NhAyFFJuXCte1qA","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01RixYL7xtqmjfDNMh8cXMM4","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","old_string":"\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}","new_string":"\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":1264,"cache_read_input_tokens":120141,"output_tokens":437,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":1264,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqMyLh7dEeuVSxHQsVv","type":"assistant","uuid":"8682e600-d7f6-46ae-9829-1dd10b04890e","timestamp":"2026-03-04T09:52:17.384Z"} +{"type":"file-history-snapshot","messageId":"8682e600-d7f6-46ae-9829-1dd10b04890e","snapshot":{"messageId":"af2b1ed5-da44-4083-a356-f7edb52c68b8","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v3","version":3,"backupTime":"2026-03-03T09:45:34.548Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v3","version":3,"backupTime":"2026-03-04T09:35:30.335Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v2","version":2,"backupTime":"2026-03-02T19:45:28.129Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/config.yaml.example":{"backupFileName":"ab8acefcb43dda84@v2","version":2,"backupTime":"2026-03-02T19:45:28.132Z"},"management/server/metrics/selfhosted.go":{"backupFileName":"e4e6cf8f05310807@v1","version":1,"backupTime":"2026-03-04T09:50:22.393Z"},"management/server/metrics/selfhosted_test.go":{"backupFileName":"a0ee876802b49262@v1","version":1,"backupTime":"2026-03-04T09:53:01.264Z"}},"timestamp":"2026-03-04T09:45:12.815Z"},"isSnapshotUpdate":true} +{"parentUuid":"8682e600-d7f6-46ae-9829-1dd10b04890e","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_01RixYL7xtqmjfDNMh8cXMM4","toolUseID":"toolu_01RixYL7xtqmjfDNMh8cXMM4","timestamp":"2026-03-04T09:53:01.270Z","uuid":"4a9ca07e-a27b-4ad6-b369-bea3e6300ed1"} +{"parentUuid":"8682e600-d7f6-46ae-9829-1dd10b04890e","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01RixYL7xtqmjfDNMh8cXMM4","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go has been updated successfully."}]},"uuid":"9313c7c9-9fc5-4b47-af76-243701a6ad59","timestamp":"2026-03-04T09:53:01.269Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","oldString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","newString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","originalFile":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n","structuredPatch":[{"oldStart":372,"oldLines":11,"newStart":372,"newLines":14,"lines":[" if properties[\"idp_users_count\"] != 1 {"," t.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])"," }","+ if properties[\"embedded_idp_users_local\"] != 1 {","+ t.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])","+ }"," if properties[\"embedded_idp_users_zitadel\"] != 1 {"," t.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])"," }","- if properties[\"embedded_idp_count\"] != 1 {","- t.Errorf(\"expected 1 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])","+ if properties[\"embedded_idp_count\"] != 2 {","+ t.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])"," }"," "," if properties[\"services\"] != 2 {"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"8682e600-d7f6-46ae-9829-1dd10b04890e"} +{"parentUuid":"9313c7c9-9fc5-4b47-af76-243701a6ad59","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01F5KbxzacGa4yFqqVRF1BAZ","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01PYPifErm78Nu1tEdvyTs8g","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","old_string":"\t\t{\"local\", \"oidc\"},\n\t\t{\"\", \"oidc\"},","new_string":"\t\t{\"local\", \"local\"},\n\t\t{\"\", \"unknown\"},"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":487,"cache_read_input_tokens":121405,"output_tokens":171,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":487,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqRok6kKhV5QZpk4eLH","type":"assistant","uuid":"6deb53f9-e9f4-415f-baba-63f851c6dbe3","timestamp":"2026-03-04T09:53:08.361Z"} +{"parentUuid":"6deb53f9-e9f4-415f-baba-63f851c6dbe3","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_01PYPifErm78Nu1tEdvyTs8g","toolUseID":"toolu_01PYPifErm78Nu1tEdvyTs8g","timestamp":"2026-03-04T09:53:15.916Z","uuid":"45711d22-f8ad-480d-b971-15f62afb175a"} +{"parentUuid":"6deb53f9-e9f4-415f-baba-63f851c6dbe3","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01PYPifErm78Nu1tEdvyTs8g","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go has been updated successfully."}]},"uuid":"91e9e989-79f9-49cd-b960-b43ce47f5baa","timestamp":"2026-03-04T09:53:15.914Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","oldString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","newString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"\", \"unknown\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","originalFile":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n","structuredPatch":[{"oldStart":439,"oldLines":8,"newStart":439,"newLines":8,"lines":[" {\"microsoft-abc123\", \"microsoft\"},"," {\"authentik-abc123\", \"authentik\"},"," {\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},","- {\"local\", \"oidc\"},","- {\"\", \"oidc\"},","+ {\"local\", \"local\"},","+ {\"\", \"unknown\"},"," }"," "," for _, tt := range tests {"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"6deb53f9-e9f4-415f-baba-63f851c6dbe3"} +{"parentUuid":"91e9e989-79f9-49cd-b960-b43ce47f5baa","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_017d8H6A8XJCKcWAa63xGKp1","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_014Y5hDbZGxMV5dTr1YrKN6L","name":"Bash","input":{"command":"go test ./management/server/metrics/... -run 'TestExtractIdpType|TestGenerateProperties' -v 2>&1","description":"Run the metrics tests","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":332,"cache_read_input_tokens":121892,"output_tokens":141,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":332,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqTEa7kWioCr8Tn4jbA","type":"assistant","uuid":"ffde5f51-975c-4c27-bfad-80314a4b8d7d","timestamp":"2026-03-04T09:53:25.232Z"} +{"parentUuid":"ffde5f51-975c-4c27-bfad-80314a4b8d7d","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":3,"totalLines":0,"totalBytes":0,"taskId":"bpezvmsw3","timeoutMs":120000},"toolUseID":"bash-progress-0","parentToolUseID":"toolu_014Y5hDbZGxMV5dTr1YrKN6L","uuid":"7ec01bc9-bde3-435e-8357-62a5f4d57162","timestamp":"2026-03-04T09:53:28.256Z"} +{"parentUuid":"7ec01bc9-bde3-435e-8357-62a5f4d57162","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":4,"totalLines":0,"totalBytes":0,"taskId":"bpezvmsw3","timeoutMs":120000},"toolUseID":"bash-progress-1","parentToolUseID":"toolu_014Y5hDbZGxMV5dTr1YrKN6L","uuid":"fcdd4bcd-8909-4b66-9a7a-4cec5127fb6b","timestamp":"2026-03-04T09:53:29.256Z"} +{"parentUuid":"fcdd4bcd-8909-4b66-9a7a-4cec5127fb6b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":5,"totalLines":0,"totalBytes":0,"taskId":"bpezvmsw3","timeoutMs":120000},"toolUseID":"bash-progress-2","parentToolUseID":"toolu_014Y5hDbZGxMV5dTr1YrKN6L","uuid":"fd4a8d2a-f7f1-4ed7-8ad9-12175795c434","timestamp":"2026-03-04T09:53:30.254Z"} +{"parentUuid":"ffde5f51-975c-4c27-bfad-80314a4b8d7d","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_014Y5hDbZGxMV5dTr1YrKN6L","type":"tool_result","content":"=== RUN TestGenerateProperties\n--- PASS: TestGenerateProperties (0.00s)\n=== RUN TestExtractIdpType\n=== RUN TestExtractIdpType/okta-abc123def\n=== RUN REDACTED\n=== RUN TestExtractIdpType/entra-xyz789\n=== RUN TestExtractIdpType/google-abc123\n=== RUN TestExtractIdpType/pocketid-abc123\n=== RUN TestExtractIdpType/microsoft-abc123\n=== RUN TestExtractIdpType/authentik-abc123\n=== RUN REDACTED\n=== RUN TestExtractIdpType/local\n=== RUN TestExtractIdpType/#00\n--- PASS: TestExtractIdpType (0.00s)\n --- PASS: TestExtractIdpType/okta-abc123def (0.00s)\n --- PASS: REDACTED (0.00s)\n --- PASS: TestExtractIdpType/entra-xyz789 (0.00s)\n --- PASS: TestExtractIdpType/google-abc123 (0.00s)\n --- PASS: TestExtractIdpType/pocketid-abc123 (0.00s)\n --- PASS: TestExtractIdpType/microsoft-abc123 (0.00s)\n --- PASS: TestExtractIdpType/authentik-abc123 (0.00s)\n --- PASS: REDACTED (0.00s)\n --- PASS: TestExtractIdpType/local (0.00s)\n --- PASS: TestExtractIdpType/#00 (0.00s)\nPASS\nok \tgithub.com/netbirdio/netbird/management/server/metrics\t0.804s","is_error":false}]},"uuid":"5d914f3a-309d-4d17-9a2b-3f682b55e092","timestamp":"2026-03-04T09:53:30.638Z","toolUseResult":{"stdout":"=== RUN TestGenerateProperties\n--- PASS: TestGenerateProperties (0.00s)\n=== RUN TestExtractIdpType\n=== RUN TestExtractIdpType/okta-abc123def\n=== RUN REDACTED\n=== RUN TestExtractIdpType/entra-xyz789\n=== RUN TestExtractIdpType/google-abc123\n=== RUN TestExtractIdpType/pocketid-abc123\n=== RUN TestExtractIdpType/microsoft-abc123\n=== RUN TestExtractIdpType/authentik-abc123\n=== RUN REDACTED\n=== RUN TestExtractIdpType/local\n=== RUN TestExtractIdpType/#00\n--- PASS: TestExtractIdpType (0.00s)\n --- PASS: TestExtractIdpType/okta-abc123def (0.00s)\n --- PASS: REDACTED (0.00s)\n --- PASS: TestExtractIdpType/entra-xyz789 (0.00s)\n --- PASS: TestExtractIdpType/google-abc123 (0.00s)\n --- PASS: TestExtractIdpType/pocketid-abc123 (0.00s)\n --- PASS: TestExtractIdpType/microsoft-abc123 (0.00s)\n --- PASS: TestExtractIdpType/authentik-abc123 (0.00s)\n --- PASS: REDACTED (0.00s)\n --- PASS: TestExtractIdpType/local (0.00s)\n --- PASS: TestExtractIdpType/#00 (0.00s)\nPASS\nok \tgithub.com/netbirdio/netbird/management/server/metrics\t0.804s","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"ffde5f51-975c-4c27-bfad-80314a4b8d7d"} +{"parentUuid":"5d914f3a-309d-4d17-9a2b-3f682b55e092","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_015XYctNY4EKrXe29YY3xGb3","type":"message","role":"assistant","content":[{"type":"text","text":"All tests pass. Here's what was fixed:\n\n**`extractIdpType`** — returned `\"oidc\"` for connector IDs without a dash (like `\"local\"`). Now returns the connector ID itself (e.g., `\"local\"` → `\"local\"`), and `\"unknown\"` for empty strings.\n\n**Counting logic** — `local` users were excluded from `embeddedIdpTypes`, so `embedded_idp_count` didn't include local as a type and no `embedded_idp_users_local` metric was emitted. Moved the `extractIdpType` + `embeddedIdpTypes` increment outside the if/else so it runs for all connector types (both local and external)."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":688,"cache_read_input_tokens":122224,"output_tokens":169,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":688,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqUM4pcNuLsimpdbJvk","type":"assistant","uuid":"d8365b81-22fc-4e51-8784-35780bf7c90b","timestamp":"2026-03-04T09:53:43.440Z"} +{"parentUuid":"d8365b81-22fc-4e51-8784-35780bf7c90b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"Stop","hookName":"Stop","command":"entire hooks claude-code stop"},"parentToolUseID":"21da6383-8523-429a-8160-e0c7e96c5aaf","toolUseID":"21da6383-8523-429a-8160-e0c7e96c5aaf","timestamp":"2026-03-04T09:53:43.450Z","uuid":"0f0b79e2-67c5-4ae6-a581-5f964aa49402"} +{"parentUuid":"0f0b79e2-67c5-4ae6-a581-5f964aa49402","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"system","subtype":"stop_hook_summary","hookCount":1,"hookInfos":[{"command":"entire hooks claude-code stop","durationMs":2975}],"hookErrors":[],"preventedContinuation":false,"stopReason":"","hasOutput":true,"level":"suggestion","timestamp":"2026-03-04T09:53:46.426Z","uuid":"fe722474-f703-4c1e-8fe6-6f694822aaf6","toolUseID":"21da6383-8523-429a-8160-e0c7e96c5aaf"} +{"parentUuid":"fe722474-f703-4c1e-8fe6-6f694822aaf6","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"system","subtype":"turn_duration","durationMs":260414,"timestamp":"2026-03-04T09:53:46.428Z","uuid":"2f170070-ae94-4374-89da-f492c1e6f34d","isMeta":false} +{"type":"file-history-snapshot","messageId":"7e7e4749-2966-4bc9-957d-998509676f55","snapshot":{"messageId":"7e7e4749-2966-4bc9-957d-998509676f55","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v3","version":3,"backupTime":"2026-03-03T09:45:34.548Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v3","version":3,"backupTime":"2026-03-04T09:35:30.335Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v2","version":2,"backupTime":"2026-03-02T19:45:28.129Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/config.yaml.example":{"backupFileName":"ab8acefcb43dda84@v2","version":2,"backupTime":"2026-03-02T19:45:28.132Z"},"management/server/metrics/selfhosted.go":{"backupFileName":"e4e6cf8f05310807@v2","version":2,"backupTime":"2026-03-04T09:54:25.163Z"},"management/server/metrics/selfhosted_test.go":{"backupFileName":"a0ee876802b49262@v2","version":2,"backupTime":"2026-03-04T09:54:25.164Z"}},"timestamp":"2026-03-04T09:54:25.160Z"},"isSnapshotUpdate":false} +{"parentUuid":"2f170070-ae94-4374-89da-f492c1e6f34d","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":"liek before, decode these: REDACTED\nREDACTED\nREDACTED\nREDACTED\nREDACTED\nREDACTED"},"uuid":"7e7e4749-2966-4bc9-957d-998509676f55","timestamp":"2026-03-04T09:54:23.565Z","permissionMode":"default"} +{"parentUuid":"7e7e4749-2966-4bc9-957d-998509676f55","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01SpbmBpG36JCYZfZb13JMzw","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01EbuJUvQ7FJEw4zfjW6gWNX","name":"Bash","input":{"command":"for encoded in \\\n \"REDACTED\" \\\n \"REDACTED\" \\\n \"REDACTED\" \\\n \"REDACTED\" \\\n \"REDACTED\" \\\n \"REDACTED\"; do\n echo \"$encoded\" | base64 -d | cat -v\n echo\ndone","description":"Decode all 6 Dex user IDs"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":555,"cache_read_input_tokens":123079,"output_tokens":612,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":555,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqXyQgjiQAd3WBYi34z","type":"assistant","uuid":"ee211e3a-7791-4316-919f-13226141d451","timestamp":"2026-03-04T09:54:34.244Z"} +{"parentUuid":"ee211e3a-7791-4316-919f-13226141d451","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01EbuJUvQ7FJEw4zfjW6gWNX","type":"tool_result","content":"$4590aa8c-b16f-4ddf-a887-200b43361fec^R^Elocal\n\n$7effea22-d1cf-41ab-9a72-75ee0cf5a211^R^Td6jvvp69kmnc73c9pl40\n\n$7effea22-d1cf-41ab-9a72-75ee0cf5a211^R^]pocketid-d67qvdm9kmnc73b2ntb0\n\n$c7097de7-04cf-415e-91c2-8eed92d4a102^R^]pocketid-d67rbam9kmnc73b2o6n0\n\n$c701d400-ff0c-4f87-b8ca-154a8001815d^R^Elocal\n\n@381d44eb2438e17b375c3c82151a6fcbbcdb0b937ad6b5893f3d2deba1e99c96^R^^authentik-d67qt8m9kmnc73b2ng","is_error":false}]},"uuid":"53b5110e-59c4-4fd6-b49e-925c66fbeae8","timestamp":"2026-03-04T09:55:42.703Z","toolUseResult":{"stdout":"$4590aa8c-b16f-4ddf-a887-200b43361fec^R^Elocal\n\n$7effea22-d1cf-41ab-9a72-75ee0cf5a211^R^Td6jvvp69kmnc73c9pl40\n\n$7effea22-d1cf-41ab-9a72-75ee0cf5a211^R^]pocketid-d67qvdm9kmnc73b2ntb0\n\n$c7097de7-04cf-415e-91c2-8eed92d4a102^R^]pocketid-d67rbam9kmnc73b2o6n0\n\n$c701d400-ff0c-4f87-b8ca-154a8001815d^R^Elocal\n\n@381d44eb2438e17b375c3c82151a6fcbbcdb0b937ad6b5893f3d2deba1e99c96^R^^authentik-d67qt8m9kmnc73b2ng","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"ee211e3a-7791-4316-919f-13226141d451"} +{"parentUuid":"53b5110e-59c4-4fd6-b49e-925c66fbeae8","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01MNm3zisHoG8N1pdpLvMPKE","type":"message","role":"assistant","content":[{"type":"text","text":"| # | Stored ID (base64) | Decoded (raw) | User ID | Connector | IdP Type |\n|---|---|---|---|---|---|\n| 1 | `REDACTED` | `\\x0a$4590aa8c-b16f-4ddf-a887-200b43361fec\\x12\\x05local` | `4590aa8c-b16f-4ddf-a887-200b43361fec` | `local` | local |\n| 2 | `REDACTED` | `\\x0a$7effea22-d1cf-41ab-9a72-75ee0cf5a211\\x12\\x14d6jvvp69kmnc73c9pl40` | `7effea22-d1cf-41ab-9a72-75ee0cf5a211` | `d6jvvp69kmnc73c9pl40` | unknown (no dash) |\n| 3 | `REDACTED` | `\\x0a$7effea22-d1cf-41ab-9a72-75ee0cf5a211\\x12\\x1dpocketid-d67qvdm9kmnc73b2ntb0` | `7effea22-d1cf-41ab-9a72-75ee0cf5a211` | `pocketid-d67qvdm9kmnc73b2ntb0` | pocketid |\n| 4 | `REDACTED` | `\\x0a$c7097de7-04cf-415e-91c2-8eed92d4a102\\x12\\x1dpocketid-d67rbam9kmnc73b2o6n0` | `c7097de7-04cf-415e-91c2-8eed92d4a102` | `pocketid-d67rbam9kmnc73b2o6n0` | pocketid |\n| 5 | `REDACTED` | `\\x0a$c701d400-ff0c-4f87-b8ca-154a8001815d\\x12\\x05local` | `c701d400-ff0c-4f87-b8ca-154a8001815d` | `local` | local |\n| 6 | `REDACTED` | `\\x0a@381d44eb...e99c96\\x12\\x1eauthentik-d67qt8m9kmnc73b2nggg` | `381d44eb2438e17b375c3c82151a6fcbbcdb0b937ad6b5893f3d2deba1e99c96` | `authentik-d67qt8m9kmnc73b2nggg` | authentik |\n\nNotable: Row 2 has the same user UUID as row 3 (`7effea22-...`) but with a different connector — `d6jvvp69kmnc73c9pl40` (a raw xid with no type prefix). This looks like a connector that was created without the ` -` naming convention, so `extractIdpType` would return `\"d6jvvp69kmnc73c9pl40\"` as-is (no dash found). This is likely a Pocket ID connector that was reconfigured (row 3 shows the same user under the properly-named `pocketid-` connector)."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":892,"cache_read_input_tokens":123634,"output_tokens":1288,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":892,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqdiYeEeFDDF1DTHHSv","type":"assistant","uuid":"7b7ed611-549b-4d1d-b13a-6e55858e178d","timestamp":"2026-03-04T09:56:00.153Z"} +{"parentUuid":"7b7ed611-549b-4d1d-b13a-6e55858e178d","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"Stop","hookName":"Stop","command":"entire hooks claude-code stop"},"parentToolUseID":"9635c128-b680-4d79-b97b-cb078b7225b0","toolUseID":"9635c128-b680-4d79-b97b-cb078b7225b0","timestamp":"2026-03-04T09:56:00.160Z","uuid":"13939b99-96af-4443-bd5b-f0c4d5ee0482"} +{"parentUuid":"13939b99-96af-4443-bd5b-f0c4d5ee0482","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"system","subtype":"stop_hook_summary","hookCount":1,"hookInfos":[{"command":"entire hooks claude-code stop","durationMs":726}],"hookErrors":[],"preventedContinuation":false,"stopReason":"","hasOutput":true,"level":"suggestion","timestamp":"2026-03-04T09:56:00.888Z","uuid":"885d410c-1bc6-4a4f-abcc-da9348232186","toolUseID":"9635c128-b680-4d79-b97b-cb078b7225b0"} +{"type":"file-history-snapshot","messageId":"371616f7-0cc5-424d-a005-93fa503bc815","snapshot":{"messageId":"371616f7-0cc5-424d-a005-93fa503bc815","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v3","version":3,"backupTime":"2026-03-03T09:45:34.548Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v3","version":3,"backupTime":"2026-03-04T09:35:30.335Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v2","version":2,"backupTime":"2026-03-02T19:45:28.129Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/config.yaml.example":{"backupFileName":"ab8acefcb43dda84@v2","version":2,"backupTime":"2026-03-02T19:45:28.132Z"},"management/server/metrics/selfhosted.go":{"backupFileName":"e4e6cf8f05310807@v2","version":2,"backupTime":"2026-03-04T09:54:25.163Z"},"management/server/metrics/selfhosted_test.go":{"backupFileName":"a0ee876802b49262@v2","version":2,"backupTime":"2026-03-04T09:54:25.164Z"}},"timestamp":"2026-03-04T09:57:34.447Z"},"isSnapshotUpdate":false} +{"parentUuid":"885d410c-1bc6-4a4f-abcc-da9348232186","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":"if there is no prefix - this is generic oidc: func generateIdentityProviderID(idpType types.IdentityProviderType) string {\n id := xid.New().String()\n\n switch idpType {\n case types.IdentityProviderTypeOkta:\n return \"okta-\" + id\n case types.IdentityProviderTypeZitadel:\n return \"zitadel-\" + id\n case types.IdentityProviderTypeEntra:\n return \"entra-\" + id\n case types.IdentityProviderTypeGoogle:\n return \"google-\" + id\n case types.IdentityProviderTypePocketID:\n return \"pocketid-\" + id\n case types.IdentityProviderTypeMicrosoft:\n return \"microsoft-\" + id\n case types.IdentityProviderTypeAuthentik:\n return \"authentik-\" + id\n case types.IdentityProviderTypeKeycloak:\n return \"keycloak-\" + id\n default:\n // Generic OIDC - no prefix\n return id\n }\n}"},"uuid":"371616f7-0cc5-424d-a005-93fa503bc815","timestamp":"2026-03-04T09:57:32.789Z","permissionMode":"default"} +{"parentUuid":"371616f7-0cc5-424d-a005-93fa503bc815","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01TDagHPLab1Bzg5mC15kiwn","type":"message","role":"assistant","content":[{"type":"text","text":"\n\nGood catch. So a connector ID with no dash is generic OIDC, not \"unknown\". The fix should return `\"oidc\"` for no-dash cases (except `\"local\"`)."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":279,"cache_read_input_tokens":125812,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":279},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhqmxgvNFsFywZNy8a88","type":"assistant","uuid":"780fe03b-1f06-43cb-9a28-d427e6459d4f","timestamp":"2026-03-04T09:57:41.866Z"} +{"parentUuid":"780fe03b-1f06-43cb-9a28-d427e6459d4f","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01TDagHPLab1Bzg5mC15kiwn","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01XtJDfSSzEqH7Bq53gaB1dX","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go","old_string":"func extractIdpType(connectorID string) string {\n\tif connectorID == \"\" {\n\t\treturn \"unknown\"\n\t}\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn strings.ToLower(connectorID)\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}","new_string":"func extractIdpType(connectorID string) string {\n\tif connectorID == \"local\" {\n\t\treturn \"local\"\n\t}\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn \"oidc\"\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":279,"cache_read_input_tokens":125812,"output_tokens":355,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":279,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqmxgvNFsFywZNy8a88","type":"assistant","uuid":"840055ea-c017-4d7c-9183-a832ab1f67a0","timestamp":"2026-03-04T09:57:44.100Z"} +{"parentUuid":"840055ea-c017-4d7c-9183-a832ab1f67a0","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_01XtJDfSSzEqH7Bq53gaB1dX","toolUseID":"toolu_01XtJDfSSzEqH7Bq53gaB1dX","timestamp":"2026-03-04T09:57:57.119Z","uuid":"3efceeb3-236c-453c-9a85-8b7fc601dbad"} +{"parentUuid":"840055ea-c017-4d7c-9183-a832ab1f67a0","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01XtJDfSSzEqH7Bq53gaB1dX","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go has been updated successfully."}]},"uuid":"ae9bd03c-6a12-4012-9cf8-2586c73953b6","timestamp":"2026-03-04T09:57:57.118Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go","oldString":"// Package metrics gather anonymous information about the usage of NetBird management\npackage metrics\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\tnbversion \"github.com/netbirdio/netbird/version\"\n)\n\nconst (\n\t// PayloadEvent identifies an event type\n\tPayloadEvent = \"self-hosted stats\"\n\t// payloadEndpoint metrics defaultEndpoint to send anonymous data\n\tpayloadEndpoint = \"https://metrics.netbird.io\"\n\t// defaultPushInterval default interval to push metrics\n\tdefaultPushInterval = 12 * time.Hour\n\t// requestTimeout http request timeout\n\trequestTimeout = 45 * time.Second\n\tEmbeddedType = \"embedded\"\n)\n\ntype getTokenResponse struct {\n\tPublicAPIToken string `json:\"public_api_token\"`\n}\n\ntype pushPayload struct {\n\tAPIKey string `json:\"api_key\"`\n\tDistinctID string `json:\"distinct_id\"`\n\tEvent string `json:\"event\"`\n\tProperties properties `json:\"properties\"`\n\tTimestamp time.Time `json:\"timestamp\"`\n}\n\n// properties metrics to push\ntype properties map[string]interface{}\n\n// DataSource metric data source\ntype DataSource interface {\n\tGetAllAccounts(ctx context.Context) []*types.Account\n\tGetStoreEngine() types.Engine\n\tGetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)\n}\n\n// ConnManager peer connection manager that holds state for current active connections\ntype ConnManager interface {\n\tGetAllConnectedPeers() map[string]struct{}\n}\n\n// Worker metrics collector and pusher\ntype Worker struct {\n\tctx context.Context\n\tid string\n\tidpManager string\n\tdataSource DataSource\n\tconnManager ConnManager\n\tstartupTime time.Time\n\tlastRun time.Time\n}\n\n// NewWorker returns a metrics worker\nfunc NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {\n\tcurrentTime := time.Now()\n\treturn &Worker{\n\t\tctx: ctx,\n\t\tid: id,\n\t\tidpManager: idpManager,\n\t\tdataSource: dataSource,\n\t\tconnManager: connManager,\n\t\tstartupTime: currentTime,\n\t\tlastRun: currentTime,\n\t}\n}\n\n// Run runs the metrics worker\nfunc (w *Worker) Run(ctx context.Context) {\n\tinterval := getMetricsInterval(ctx)\n\n\tpushTicker := time.NewTicker(interval)\n\tfor {\n\t\tselect {\n\t\tcase <-w.ctx.Done():\n\t\t\treturn\n\t\tcase <-pushTicker.C:\n\t\t\terr := w.sendMetrics(ctx)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithContext(ctx).Error(err)\n\t\t\t}\n\t\t\tw.lastRun = time.Now()\n\t\t}\n\t}\n}\n\nfunc getMetricsInterval(ctx context.Context) time.Duration {\n\tinterval := defaultPushInterval\n\tif os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") != \"\" {\n\t\tnewInterval, err := time.ParseDuration(os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") + \"s\")\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"unable to parse NETBIRD_METRICS_INTERVAL_IN_SECONDS, using default interval %v. Error: %v\", defaultPushInterval, err)\n\t\t} else {\n\t\t\tlog.WithContext(ctx).Infof(\"using NETBIRD_METRICS_INTERVAL_IN_SECONDS %s\", newInterval)\n\t\t\tinterval = newInterval\n\t\t}\n\t}\n\treturn interval\n}\n\nfunc (w *Worker) sendMetrics(ctx context.Context) error {\n\tapiKey, err := getAPIKey(w.ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpayload := w.generatePayload(ctx, apiKey)\n\n\tpayloadString, err := buildMetricsPayload(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thttpClient := http.Client{}\n\n\texportJobReq, cancelCTX, err := createPostRequest(w.ctx, payloadEndpoint+\"/capture/\", payloadString)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create metrics post request %v\", err)\n\t}\n\tdefer cancelCTX()\n\n\tjobResp, err := httpClient.Do(exportJobReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to push metrics %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = jobResp.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing update metrics response body: %v\", err)\n\t\t}\n\t}()\n\n\tif jobResp.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"unable to push anonymous metrics, got statusCode %d\", jobResp.StatusCode)\n\t}\n\n\tlog.WithContext(ctx).Infof(\"sent anonymous metrics, next push will happen in %s. \"+\n\t\t\"You can disable these metrics by running with flag --disable-anonymous-metrics,\"+\n\t\t\" see more information at https://docs.netbird.io/about-netbird/faq#why-and-what-are-the-anonymous-usage-metrics\", getMetricsInterval(ctx))\n\n\treturn nil\n}\n\nfunc (w *Worker) generatePayload(ctx context.Context, apiKey string) pushPayload {\n\tproperties := w.generateProperties(ctx)\n\n\treturn pushPayload{\n\t\tAPIKey: apiKey,\n\t\tDistinctID: w.id,\n\t\tEvent: PayloadEvent,\n\t\tProperties: properties,\n\t\tTimestamp: time.Now(),\n\t}\n}\n\nfunc (w *Worker) generateProperties(ctx context.Context) properties {\n\tvar (\n\t\tuptime float64\n\t\taccounts int\n\t\texpirationEnabled int\n\t\tusers int\n\t\tserviceUsers int\n\t\tpats int\n\t\tpeers int\n\t\tpeersSSHEnabled int\n\t\tsetupKeysUsage int\n\t\tephemeralPeersSKs int\n\t\tephemeralPeersSKUsage int\n\t\tactivePeersLastDay int\n\t\tactiveUserPeersLastDay int\n\t\tosPeers map[string]int\n\t\tactiveUsersLastDay map[string]struct{}\n\t\tuserPeers int\n\t\trules int\n\t\trulesProtocol map[string]int\n\t\trulesDirection map[string]int\n\t\trulesWithSrcPostureChecks int\n\t\tpostureChecks int\n\t\tgroups int\n\t\troutes int\n\t\troutesWithRGGroups int\n\t\tnetworks int\n\t\tnetworkResources int\n\t\tnetworkRouters int\n\t\tnetworkRoutersWithPG int\n\t\tnameservers int\n\t\tuiClient int\n\t\tversion string\n\t\tpeerActiveVersions []string\n\t\tosUIClients map[string]int\n\t\trosenpassEnabled int\n\t\tlocalUsers int\n\t\tidpUsers int\n\t\tembeddedIdpTypes map[string]int\n\t\tservices int\n\t\tservicesEnabled int\n\t\tservicesTargets int\n\t\tservicesStatusActive int\n\t\tservicesStatusPending int\n\t\tservicesStatusError int\n\t\tservicesTargetType map[string]int\n\t\tservicesAuthPassword int\n\t\tservicesAuthPin int\n\t\tservicesAuthOIDC int\n\t)\n\tstart := time.Now()\n\tmetricsProperties := make(properties)\n\tosPeers = make(map[string]int)\n\tosUIClients = make(map[string]int)\n\trulesProtocol = make(map[string]int)\n\trulesDirection = make(map[string]int)\n\tactiveUsersLastDay = make(map[string]struct{})\n\tembeddedIdpTypes = make(map[string]int)\n\tservicesTargetType = make(map[string]int)\n\tuptime = time.Since(w.startupTime).Seconds()\n\tconnections := w.connManager.GetAllConnectedPeers()\n\tversion = nbversion.NetbirdVersion()\n\n\tcustomDomains, customDomainsValidated, _ := w.dataSource.GetCustomDomainsCounts(ctx)\n\n\tfor _, account := range w.dataSource.GetAllAccounts(ctx) {\n\t\taccounts++\n\n\t\tif account.Settings.PeerLoginExpirationEnabled {\n\t\t\texpirationEnabled++\n\t\t}\n\n\t\tgroups += len(account.Groups)\n\t\tnetworks += len(account.Networks)\n\t\tnetworkResources += len(account.NetworkResources)\n\n\t\tnetworkRouters += len(account.NetworkRouters)\n\t\tfor _, router := range account.NetworkRouters {\n\t\t\tif len(router.PeerGroups) > 0 {\n\t\t\t\tnetworkRoutersWithPG++\n\t\t\t}\n\t\t}\n\n\t\troutes += len(account.Routes)\n\t\tfor _, route := range account.Routes {\n\t\t\tif len(route.PeerGroups) > 0 {\n\t\t\t\troutesWithRGGroups++\n\t\t\t}\n\t\t}\n\t\tnameservers += len(account.NameServerGroups)\n\n\t\tfor _, policy := range account.Policies {\n\t\t\tfor _, rule := range policy.Rules {\n\t\t\t\trules++\n\t\t\t\trulesProtocol[string(rule.Protocol)]++\n\t\t\t\tif rule.Bidirectional {\n\t\t\t\t\trulesDirection[\"bidirectional\"]++\n\t\t\t\t} else {\n\t\t\t\t\trulesDirection[\"oneway\"]++\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(policy.SourcePostureChecks) > 0 {\n\t\t\t\trulesWithSrcPostureChecks++\n\t\t\t}\n\t\t}\n\n\t\tpostureChecks += len(account.PostureChecks)\n\n\t\tfor _, user := range account.Users {\n\t\t\tif user.IsServiceUser {\n\t\t\t\tserviceUsers++\n\t\t\t} else {\n\t\t\t\tusers++\n\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t}\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpats += len(user.PATs)\n\t\t}\n\n\t\tfor _, key := range account.SetupKeys {\n\t\t\tsetupKeysUsage += key.UsedTimes\n\t\t\tif key.Ephemeral {\n\t\t\t\tephemeralPeersSKs++\n\t\t\t\tephemeralPeersSKUsage += key.UsedTimes\n\t\t\t}\n\t\t}\n\n\t\tfor _, peer := range account.Peers {\n\t\t\tpeers++\n\n\t\t\tif peer.SSHEnabled || peer.Meta.Flags.ServerSSHAllowed {\n\t\t\t\tpeersSSHEnabled++\n\t\t\t}\n\n\t\t\tif peer.Meta.Flags.RosenpassEnabled {\n\t\t\t\trosenpassEnabled++\n\t\t\t}\n\n\t\t\tif peer.UserID != \"\" {\n\t\t\t\tuserPeers++\n\t\t\t}\n\n\t\t\tosKey := strings.ToLower(fmt.Sprintf(\"peer_os_%s\", peer.Meta.GoOS))\n\t\t\tosCount := osPeers[osKey]\n\t\t\tosPeers[osKey] = osCount + 1\n\n\t\t\tif peer.Meta.UIVersion != \"\" {\n\t\t\t\tuiClient++\n\t\t\t\tuiOSKey := strings.ToLower(fmt.Sprintf(\"ui_client_os_%s\", peer.Meta.GoOS))\n\t\t\t\tosUICount := osUIClients[uiOSKey]\n\t\t\t\tosUIClients[uiOSKey] = osUICount + 1\n\t\t\t}\n\n\t\t\t_, connected := connections[peer.ID]\n\t\t\tif connected || peer.Status.LastSeen.After(w.lastRun) {\n\t\t\t\tactivePeersLastDay++\n\t\t\t\tif peer.UserID != \"\" {\n\t\t\t\t\tactiveUserPeersLastDay++\n\t\t\t\t\tactiveUsersLastDay[peer.UserID] = struct{}{}\n\t\t\t\t}\n\t\t\t\tosActiveKey := osKey + \"_active\"\n\t\t\t\tosActiveCount := osPeers[osActiveKey]\n\t\t\t\tosPeers[osActiveKey] = osActiveCount + 1\n\t\t\t\tpeerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)\n\t\t\t}\n\t\t}\n\n\t\tfor _, service := range account.Services {\n\t\t\tservices++\n\t\t\tif service.Enabled {\n\t\t\t\tservicesEnabled++\n\t\t\t}\n\t\t\tservicesTargets += len(service.Targets)\n\n\t\t\tswitch rpservice.Status(service.Meta.Status) {\n\t\t\tcase rpservice.StatusActive:\n\t\t\t\tservicesStatusActive++\n\t\t\tcase rpservice.StatusPending:\n\t\t\t\tservicesStatusPending++\n\t\t\tcase rpservice.StatusError, rpservice.StatusCertificateFailed, rpservice.StatusTunnelNotCreated:\n\t\t\t\tservicesStatusError++\n\t\t\t}\n\n\t\t\tfor _, target := range service.Targets {\n\t\t\t\tservicesTargetType[target.TargetType]++\n\t\t\t}\n\n\t\t\tif service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled {\n\t\t\t\tservicesAuthPassword++\n\t\t\t}\n\t\t\tif service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled {\n\t\t\t\tservicesAuthPin++\n\t\t\t}\n\t\t\tif service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled {\n\t\t\t\tservicesAuthOIDC++\n\t\t\t}\n\t\t}\n\t}\n\n\tminActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)\n\tmetricsProperties[\"uptime\"] = uptime\n\tmetricsProperties[\"accounts\"] = accounts\n\tmetricsProperties[\"users\"] = users\n\tmetricsProperties[\"service_users\"] = serviceUsers\n\tmetricsProperties[\"pats\"] = pats\n\tmetricsProperties[\"peers\"] = peers\n\tmetricsProperties[\"peers_ssh_enabled\"] = peersSSHEnabled\n\tmetricsProperties[\"peers_login_expiration_enabled\"] = expirationEnabled\n\tmetricsProperties[\"setup_keys_usage\"] = setupKeysUsage\n\tmetricsProperties[\"ephemeral_peers_setup_keys\"] = ephemeralPeersSKs\n\tmetricsProperties[\"ephemeral_peers_setup_keys_usage\"] = ephemeralPeersSKUsage\n\tmetricsProperties[\"active_peers_last_day\"] = activePeersLastDay\n\tmetricsProperties[\"active_user_peers_last_day\"] = activeUserPeersLastDay\n\tmetricsProperties[\"active_users_last_day\"] = len(activeUsersLastDay)\n\tmetricsProperties[\"user_peers\"] = userPeers\n\tmetricsProperties[\"rules\"] = rules\n\tmetricsProperties[\"rules_with_src_posture_checks\"] = rulesWithSrcPostureChecks\n\tmetricsProperties[\"posture_checks\"] = postureChecks\n\tmetricsProperties[\"groups\"] = groups\n\tmetricsProperties[\"networks\"] = networks\n\tmetricsProperties[\"network_resources\"] = networkResources\n\tmetricsProperties[\"network_routers\"] = networkRouters\n\tmetricsProperties[\"network_routers_with_groups\"] = networkRoutersWithPG\n\tmetricsProperties[\"routes\"] = routes\n\tmetricsProperties[\"routes_with_routing_groups\"] = routesWithRGGroups\n\tmetricsProperties[\"nameservers\"] = nameservers\n\tmetricsProperties[\"version\"] = version\n\tmetricsProperties[\"min_active_peer_version\"] = minActivePeerVersion\n\tmetricsProperties[\"max_active_peer_version\"] = maxActivePeerVersion\n\tmetricsProperties[\"ui_clients\"] = uiClient\n\tmetricsProperties[\"idp_manager\"] = w.idpManager\n\tmetricsProperties[\"store_engine\"] = w.dataSource.GetStoreEngine()\n\tmetricsProperties[\"rosenpass_enabled\"] = rosenpassEnabled\n\tmetricsProperties[\"local_users_count\"] = localUsers\n\tmetricsProperties[\"idp_users_count\"] = idpUsers\n\tmetricsProperties[\"embedded_idp_count\"] = len(embeddedIdpTypes)\n\n\tmetricsProperties[\"services\"] = services\n\tmetricsProperties[\"services_enabled\"] = servicesEnabled\n\tmetricsProperties[\"services_targets\"] = servicesTargets\n\tmetricsProperties[\"services_status_active\"] = servicesStatusActive\n\tmetricsProperties[\"services_status_pending\"] = servicesStatusPending\n\tmetricsProperties[\"services_status_error\"] = servicesStatusError\n\tmetricsProperties[\"services_auth_password\"] = servicesAuthPassword\n\tmetricsProperties[\"services_auth_pin\"] = servicesAuthPin\n\tmetricsProperties[\"services_auth_oidc\"] = servicesAuthOIDC\n\tmetricsProperties[\"custom_domains\"] = customDomains\n\tmetricsProperties[\"custom_domains_validated\"] = customDomainsValidated\n\n\tfor targetType, count := range servicesTargetType {\n\t\tmetricsProperties[\"services_target_type_\"+targetType] = count\n\t}\n\n\tfor idpType, count := range embeddedIdpTypes {\n\t\tmetricsProperties[\"embedded_idp_users_\"+idpType] = count\n\t}\n\n\tfor protocol, count := range rulesProtocol {\n\t\tmetricsProperties[\"rules_protocol_\"+protocol] = count\n\t}\n\n\tfor direction, count := range rulesDirection {\n\t\tmetricsProperties[\"rules_direction_\"+direction] = count\n\t}\n\n\tfor os, count := range osPeers {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tfor os, count := range osUIClients {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tmetricsProperties[\"metric_generation_time\"] = time.Since(start).Milliseconds()\n\n\treturn metricsProperties\n}\n\nfunc getAPIKey(ctx context.Context) (string, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\tdefer cancel()\n\n\thttpClient := http.Client{}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+\"/GetToken\", nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create request for metrics public api token %v\", err)\n\t}\n\tresponse, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to request metrics public api token %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing metrics token response body: %v\", err)\n\t\t}\n\t}()\n\n\tif response.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"unable to retrieve metrics token, statusCode %d\", response.StatusCode)\n\t}\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't get metrics token response; %v\", err)\n\t}\n\n\tvar tokenResponse getTokenResponse\n\n\terr = json.Unmarshal(body, &tokenResponse)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't parse metrics public api token; %v\", err)\n\t}\n\n\treturn tokenResponse.PublicAPIToken, nil\n}\n\nfunc buildMetricsPayload(payload pushPayload) (string, error) {\n\tstr, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to marshal metrics payload, got err: %v\", err)\n\t}\n\treturn string(str), nil\n}\n\nfunc createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, context.CancelFunc, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\treqURL := endpoint\n\n\tpayload := strings.NewReader(payloadStr)\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", reqURL, payload)\n\tif err != nil {\n\t\tcancel()\n\t\treturn nil, nil, err\n\t}\n\treq.Header.Add(\"content-type\", \"application/json\")\n\n\treturn req, cancel, nil\n}\n\n// extractIdpType extracts the IdP type from a Dex connector ID.\n// Connector IDs are formatted as \" - \" (e.g., \"okta-abc123\", \"zitadel-xyz\").\n// Returns the type prefix, or \"oidc\" if no known prefix is found.\nfunc extractIdpType(connectorID string) string {\n\tif connectorID == \"\" {\n\t\treturn \"unknown\"\n\t}\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn strings.ToLower(connectorID)\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}\n\nfunc getMinMaxVersion(inputList []string) (string, string) {\n\tversions := make([]*version.Version, 0)\n\n\tfor _, raw := range inputList {\n\t\tif raw != \"\" && nbversion.SemverRegexp.MatchString(raw) {\n\t\t\tv, err := version.NewVersion(raw)\n\t\t\tif err == nil {\n\t\t\t\tversions = append(versions, v)\n\t\t\t}\n\t\t}\n\t}\n\n\ttargetIndex := 1\n\tl := len(versions)\n\n\tswitch l {\n\tcase 0:\n\t\treturn \"\", \"\"\n\tcase targetIndex:\n\t\tv := versions[targetIndex-1].String()\n\t\treturn v, v\n\tdefault:\n\t\tsort.Sort(version.Collection(versions))\n\t\treturn versions[targetIndex-1].String(), versions[l-1].String()\n\t}\n}","newString":"// Package metrics gather anonymous information about the usage of NetBird management\npackage metrics\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\tnbversion \"github.com/netbirdio/netbird/version\"\n)\n\nconst (\n\t// PayloadEvent identifies an event type\n\tPayloadEvent = \"self-hosted stats\"\n\t// payloadEndpoint metrics defaultEndpoint to send anonymous data\n\tpayloadEndpoint = \"https://metrics.netbird.io\"\n\t// defaultPushInterval default interval to push metrics\n\tdefaultPushInterval = 12 * time.Hour\n\t// requestTimeout http request timeout\n\trequestTimeout = 45 * time.Second\n\tEmbeddedType = \"embedded\"\n)\n\ntype getTokenResponse struct {\n\tPublicAPIToken string `json:\"public_api_token\"`\n}\n\ntype pushPayload struct {\n\tAPIKey string `json:\"api_key\"`\n\tDistinctID string `json:\"distinct_id\"`\n\tEvent string `json:\"event\"`\n\tProperties properties `json:\"properties\"`\n\tTimestamp time.Time `json:\"timestamp\"`\n}\n\n// properties metrics to push\ntype properties map[string]interface{}\n\n// DataSource metric data source\ntype DataSource interface {\n\tGetAllAccounts(ctx context.Context) []*types.Account\n\tGetStoreEngine() types.Engine\n\tGetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)\n}\n\n// ConnManager peer connection manager that holds state for current active connections\ntype ConnManager interface {\n\tGetAllConnectedPeers() map[string]struct{}\n}\n\n// Worker metrics collector and pusher\ntype Worker struct {\n\tctx context.Context\n\tid string\n\tidpManager string\n\tdataSource DataSource\n\tconnManager ConnManager\n\tstartupTime time.Time\n\tlastRun time.Time\n}\n\n// NewWorker returns a metrics worker\nfunc NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {\n\tcurrentTime := time.Now()\n\treturn &Worker{\n\t\tctx: ctx,\n\t\tid: id,\n\t\tidpManager: idpManager,\n\t\tdataSource: dataSource,\n\t\tconnManager: connManager,\n\t\tstartupTime: currentTime,\n\t\tlastRun: currentTime,\n\t}\n}\n\n// Run runs the metrics worker\nfunc (w *Worker) Run(ctx context.Context) {\n\tinterval := getMetricsInterval(ctx)\n\n\tpushTicker := time.NewTicker(interval)\n\tfor {\n\t\tselect {\n\t\tcase <-w.ctx.Done():\n\t\t\treturn\n\t\tcase <-pushTicker.C:\n\t\t\terr := w.sendMetrics(ctx)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithContext(ctx).Error(err)\n\t\t\t}\n\t\t\tw.lastRun = time.Now()\n\t\t}\n\t}\n}\n\nfunc getMetricsInterval(ctx context.Context) time.Duration {\n\tinterval := defaultPushInterval\n\tif os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") != \"\" {\n\t\tnewInterval, err := time.ParseDuration(os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") + \"s\")\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"unable to parse NETBIRD_METRICS_INTERVAL_IN_SECONDS, using default interval %v. Error: %v\", defaultPushInterval, err)\n\t\t} else {\n\t\t\tlog.WithContext(ctx).Infof(\"using NETBIRD_METRICS_INTERVAL_IN_SECONDS %s\", newInterval)\n\t\t\tinterval = newInterval\n\t\t}\n\t}\n\treturn interval\n}\n\nfunc (w *Worker) sendMetrics(ctx context.Context) error {\n\tapiKey, err := getAPIKey(w.ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpayload := w.generatePayload(ctx, apiKey)\n\n\tpayloadString, err := buildMetricsPayload(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thttpClient := http.Client{}\n\n\texportJobReq, cancelCTX, err := createPostRequest(w.ctx, payloadEndpoint+\"/capture/\", payloadString)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create metrics post request %v\", err)\n\t}\n\tdefer cancelCTX()\n\n\tjobResp, err := httpClient.Do(exportJobReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to push metrics %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = jobResp.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing update metrics response body: %v\", err)\n\t\t}\n\t}()\n\n\tif jobResp.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"unable to push anonymous metrics, got statusCode %d\", jobResp.StatusCode)\n\t}\n\n\tlog.WithContext(ctx).Infof(\"sent anonymous metrics, next push will happen in %s. \"+\n\t\t\"You can disable these metrics by running with flag --disable-anonymous-metrics,\"+\n\t\t\" see more information at https://docs.netbird.io/about-netbird/faq#why-and-what-are-the-anonymous-usage-metrics\", getMetricsInterval(ctx))\n\n\treturn nil\n}\n\nfunc (w *Worker) generatePayload(ctx context.Context, apiKey string) pushPayload {\n\tproperties := w.generateProperties(ctx)\n\n\treturn pushPayload{\n\t\tAPIKey: apiKey,\n\t\tDistinctID: w.id,\n\t\tEvent: PayloadEvent,\n\t\tProperties: properties,\n\t\tTimestamp: time.Now(),\n\t}\n}\n\nfunc (w *Worker) generateProperties(ctx context.Context) properties {\n\tvar (\n\t\tuptime float64\n\t\taccounts int\n\t\texpirationEnabled int\n\t\tusers int\n\t\tserviceUsers int\n\t\tpats int\n\t\tpeers int\n\t\tpeersSSHEnabled int\n\t\tsetupKeysUsage int\n\t\tephemeralPeersSKs int\n\t\tephemeralPeersSKUsage int\n\t\tactivePeersLastDay int\n\t\tactiveUserPeersLastDay int\n\t\tosPeers map[string]int\n\t\tactiveUsersLastDay map[string]struct{}\n\t\tuserPeers int\n\t\trules int\n\t\trulesProtocol map[string]int\n\t\trulesDirection map[string]int\n\t\trulesWithSrcPostureChecks int\n\t\tpostureChecks int\n\t\tgroups int\n\t\troutes int\n\t\troutesWithRGGroups int\n\t\tnetworks int\n\t\tnetworkResources int\n\t\tnetworkRouters int\n\t\tnetworkRoutersWithPG int\n\t\tnameservers int\n\t\tuiClient int\n\t\tversion string\n\t\tpeerActiveVersions []string\n\t\tosUIClients map[string]int\n\t\trosenpassEnabled int\n\t\tlocalUsers int\n\t\tidpUsers int\n\t\tembeddedIdpTypes map[string]int\n\t\tservices int\n\t\tservicesEnabled int\n\t\tservicesTargets int\n\t\tservicesStatusActive int\n\t\tservicesStatusPending int\n\t\tservicesStatusError int\n\t\tservicesTargetType map[string]int\n\t\tservicesAuthPassword int\n\t\tservicesAuthPin int\n\t\tservicesAuthOIDC int\n\t)\n\tstart := time.Now()\n\tmetricsProperties := make(properties)\n\tosPeers = make(map[string]int)\n\tosUIClients = make(map[string]int)\n\trulesProtocol = make(map[string]int)\n\trulesDirection = make(map[string]int)\n\tactiveUsersLastDay = make(map[string]struct{})\n\tembeddedIdpTypes = make(map[string]int)\n\tservicesTargetType = make(map[string]int)\n\tuptime = time.Since(w.startupTime).Seconds()\n\tconnections := w.connManager.GetAllConnectedPeers()\n\tversion = nbversion.NetbirdVersion()\n\n\tcustomDomains, customDomainsValidated, _ := w.dataSource.GetCustomDomainsCounts(ctx)\n\n\tfor _, account := range w.dataSource.GetAllAccounts(ctx) {\n\t\taccounts++\n\n\t\tif account.Settings.PeerLoginExpirationEnabled {\n\t\t\texpirationEnabled++\n\t\t}\n\n\t\tgroups += len(account.Groups)\n\t\tnetworks += len(account.Networks)\n\t\tnetworkResources += len(account.NetworkResources)\n\n\t\tnetworkRouters += len(account.NetworkRouters)\n\t\tfor _, router := range account.NetworkRouters {\n\t\t\tif len(router.PeerGroups) > 0 {\n\t\t\t\tnetworkRoutersWithPG++\n\t\t\t}\n\t\t}\n\n\t\troutes += len(account.Routes)\n\t\tfor _, route := range account.Routes {\n\t\t\tif len(route.PeerGroups) > 0 {\n\t\t\t\troutesWithRGGroups++\n\t\t\t}\n\t\t}\n\t\tnameservers += len(account.NameServerGroups)\n\n\t\tfor _, policy := range account.Policies {\n\t\t\tfor _, rule := range policy.Rules {\n\t\t\t\trules++\n\t\t\t\trulesProtocol[string(rule.Protocol)]++\n\t\t\t\tif rule.Bidirectional {\n\t\t\t\t\trulesDirection[\"bidirectional\"]++\n\t\t\t\t} else {\n\t\t\t\t\trulesDirection[\"oneway\"]++\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(policy.SourcePostureChecks) > 0 {\n\t\t\t\trulesWithSrcPostureChecks++\n\t\t\t}\n\t\t}\n\n\t\tpostureChecks += len(account.PostureChecks)\n\n\t\tfor _, user := range account.Users {\n\t\t\tif user.IsServiceUser {\n\t\t\t\tserviceUsers++\n\t\t\t} else {\n\t\t\t\tusers++\n\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t}\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpats += len(user.PATs)\n\t\t}\n\n\t\tfor _, key := range account.SetupKeys {\n\t\t\tsetupKeysUsage += key.UsedTimes\n\t\t\tif key.Ephemeral {\n\t\t\t\tephemeralPeersSKs++\n\t\t\t\tephemeralPeersSKUsage += key.UsedTimes\n\t\t\t}\n\t\t}\n\n\t\tfor _, peer := range account.Peers {\n\t\t\tpeers++\n\n\t\t\tif peer.SSHEnabled || peer.Meta.Flags.ServerSSHAllowed {\n\t\t\t\tpeersSSHEnabled++\n\t\t\t}\n\n\t\t\tif peer.Meta.Flags.RosenpassEnabled {\n\t\t\t\trosenpassEnabled++\n\t\t\t}\n\n\t\t\tif peer.UserID != \"\" {\n\t\t\t\tuserPeers++\n\t\t\t}\n\n\t\t\tosKey := strings.ToLower(fmt.Sprintf(\"peer_os_%s\", peer.Meta.GoOS))\n\t\t\tosCount := osPeers[osKey]\n\t\t\tosPeers[osKey] = osCount + 1\n\n\t\t\tif peer.Meta.UIVersion != \"\" {\n\t\t\t\tuiClient++\n\t\t\t\tuiOSKey := strings.ToLower(fmt.Sprintf(\"ui_client_os_%s\", peer.Meta.GoOS))\n\t\t\t\tosUICount := osUIClients[uiOSKey]\n\t\t\t\tosUIClients[uiOSKey] = osUICount + 1\n\t\t\t}\n\n\t\t\t_, connected := connections[peer.ID]\n\t\t\tif connected || peer.Status.LastSeen.After(w.lastRun) {\n\t\t\t\tactivePeersLastDay++\n\t\t\t\tif peer.UserID != \"\" {\n\t\t\t\t\tactiveUserPeersLastDay++\n\t\t\t\t\tactiveUsersLastDay[peer.UserID] = struct{}{}\n\t\t\t\t}\n\t\t\t\tosActiveKey := osKey + \"_active\"\n\t\t\t\tosActiveCount := osPeers[osActiveKey]\n\t\t\t\tosPeers[osActiveKey] = osActiveCount + 1\n\t\t\t\tpeerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)\n\t\t\t}\n\t\t}\n\n\t\tfor _, service := range account.Services {\n\t\t\tservices++\n\t\t\tif service.Enabled {\n\t\t\t\tservicesEnabled++\n\t\t\t}\n\t\t\tservicesTargets += len(service.Targets)\n\n\t\t\tswitch rpservice.Status(service.Meta.Status) {\n\t\t\tcase rpservice.StatusActive:\n\t\t\t\tservicesStatusActive++\n\t\t\tcase rpservice.StatusPending:\n\t\t\t\tservicesStatusPending++\n\t\t\tcase rpservice.StatusError, rpservice.StatusCertificateFailed, rpservice.StatusTunnelNotCreated:\n\t\t\t\tservicesStatusError++\n\t\t\t}\n\n\t\t\tfor _, target := range service.Targets {\n\t\t\t\tservicesTargetType[target.TargetType]++\n\t\t\t}\n\n\t\t\tif service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled {\n\t\t\t\tservicesAuthPassword++\n\t\t\t}\n\t\t\tif service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled {\n\t\t\t\tservicesAuthPin++\n\t\t\t}\n\t\t\tif service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled {\n\t\t\t\tservicesAuthOIDC++\n\t\t\t}\n\t\t}\n\t}\n\n\tminActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)\n\tmetricsProperties[\"uptime\"] = uptime\n\tmetricsProperties[\"accounts\"] = accounts\n\tmetricsProperties[\"users\"] = users\n\tmetricsProperties[\"service_users\"] = serviceUsers\n\tmetricsProperties[\"pats\"] = pats\n\tmetricsProperties[\"peers\"] = peers\n\tmetricsProperties[\"peers_ssh_enabled\"] = peersSSHEnabled\n\tmetricsProperties[\"peers_login_expiration_enabled\"] = expirationEnabled\n\tmetricsProperties[\"setup_keys_usage\"] = setupKeysUsage\n\tmetricsProperties[\"ephemeral_peers_setup_keys\"] = ephemeralPeersSKs\n\tmetricsProperties[\"ephemeral_peers_setup_keys_usage\"] = ephemeralPeersSKUsage\n\tmetricsProperties[\"active_peers_last_day\"] = activePeersLastDay\n\tmetricsProperties[\"active_user_peers_last_day\"] = activeUserPeersLastDay\n\tmetricsProperties[\"active_users_last_day\"] = len(activeUsersLastDay)\n\tmetricsProperties[\"user_peers\"] = userPeers\n\tmetricsProperties[\"rules\"] = rules\n\tmetricsProperties[\"rules_with_src_posture_checks\"] = rulesWithSrcPostureChecks\n\tmetricsProperties[\"posture_checks\"] = postureChecks\n\tmetricsProperties[\"groups\"] = groups\n\tmetricsProperties[\"networks\"] = networks\n\tmetricsProperties[\"network_resources\"] = networkResources\n\tmetricsProperties[\"network_routers\"] = networkRouters\n\tmetricsProperties[\"network_routers_with_groups\"] = networkRoutersWithPG\n\tmetricsProperties[\"routes\"] = routes\n\tmetricsProperties[\"routes_with_routing_groups\"] = routesWithRGGroups\n\tmetricsProperties[\"nameservers\"] = nameservers\n\tmetricsProperties[\"version\"] = version\n\tmetricsProperties[\"min_active_peer_version\"] = minActivePeerVersion\n\tmetricsProperties[\"max_active_peer_version\"] = maxActivePeerVersion\n\tmetricsProperties[\"ui_clients\"] = uiClient\n\tmetricsProperties[\"idp_manager\"] = w.idpManager\n\tmetricsProperties[\"store_engine\"] = w.dataSource.GetStoreEngine()\n\tmetricsProperties[\"rosenpass_enabled\"] = rosenpassEnabled\n\tmetricsProperties[\"local_users_count\"] = localUsers\n\tmetricsProperties[\"idp_users_count\"] = idpUsers\n\tmetricsProperties[\"embedded_idp_count\"] = len(embeddedIdpTypes)\n\n\tmetricsProperties[\"services\"] = services\n\tmetricsProperties[\"services_enabled\"] = servicesEnabled\n\tmetricsProperties[\"services_targets\"] = servicesTargets\n\tmetricsProperties[\"services_status_active\"] = servicesStatusActive\n\tmetricsProperties[\"services_status_pending\"] = servicesStatusPending\n\tmetricsProperties[\"services_status_error\"] = servicesStatusError\n\tmetricsProperties[\"services_auth_password\"] = servicesAuthPassword\n\tmetricsProperties[\"services_auth_pin\"] = servicesAuthPin\n\tmetricsProperties[\"services_auth_oidc\"] = servicesAuthOIDC\n\tmetricsProperties[\"custom_domains\"] = customDomains\n\tmetricsProperties[\"custom_domains_validated\"] = customDomainsValidated\n\n\tfor targetType, count := range servicesTargetType {\n\t\tmetricsProperties[\"services_target_type_\"+targetType] = count\n\t}\n\n\tfor idpType, count := range embeddedIdpTypes {\n\t\tmetricsProperties[\"embedded_idp_users_\"+idpType] = count\n\t}\n\n\tfor protocol, count := range rulesProtocol {\n\t\tmetricsProperties[\"rules_protocol_\"+protocol] = count\n\t}\n\n\tfor direction, count := range rulesDirection {\n\t\tmetricsProperties[\"rules_direction_\"+direction] = count\n\t}\n\n\tfor os, count := range osPeers {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tfor os, count := range osUIClients {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tmetricsProperties[\"metric_generation_time\"] = time.Since(start).Milliseconds()\n\n\treturn metricsProperties\n}\n\nfunc getAPIKey(ctx context.Context) (string, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\tdefer cancel()\n\n\thttpClient := http.Client{}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+\"/GetToken\", nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create request for metrics public api token %v\", err)\n\t}\n\tresponse, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to request metrics public api token %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing metrics token response body: %v\", err)\n\t\t}\n\t}()\n\n\tif response.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"unable to retrieve metrics token, statusCode %d\", response.StatusCode)\n\t}\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't get metrics token response; %v\", err)\n\t}\n\n\tvar tokenResponse getTokenResponse\n\n\terr = json.Unmarshal(body, &tokenResponse)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't parse metrics public api token; %v\", err)\n\t}\n\n\treturn tokenResponse.PublicAPIToken, nil\n}\n\nfunc buildMetricsPayload(payload pushPayload) (string, error) {\n\tstr, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to marshal metrics payload, got err: %v\", err)\n\t}\n\treturn string(str), nil\n}\n\nfunc createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, context.CancelFunc, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\treqURL := endpoint\n\n\tpayload := strings.NewReader(payloadStr)\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", reqURL, payload)\n\tif err != nil {\n\t\tcancel()\n\t\treturn nil, nil, err\n\t}\n\treq.Header.Add(\"content-type\", \"application/json\")\n\n\treturn req, cancel, nil\n}\n\n// extractIdpType extracts the IdP type from a Dex connector ID.\n// Connector IDs are formatted as \" - \" (e.g., \"okta-abc123\", \"zitadel-xyz\").\n// Returns the type prefix, or \"oidc\" if no known prefix is found.\nfunc extractIdpType(connectorID string) string {\n\tif connectorID == \"local\" {\n\t\treturn \"local\"\n\t}\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn \"oidc\"\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}\n\nfunc getMinMaxVersion(inputList []string) (string, string) {\n\tversions := make([]*version.Version, 0)\n\n\tfor _, raw := range inputList {\n\t\tif raw != \"\" && nbversion.SemverRegexp.MatchString(raw) {\n\t\t\tv, err := version.NewVersion(raw)\n\t\t\tif err == nil {\n\t\t\t\tversions = append(versions, v)\n\t\t\t}\n\t\t}\n\t}\n\n\ttargetIndex := 1\n\tl := len(versions)\n\n\tswitch l {\n\tcase 0:\n\t\treturn \"\", \"\"\n\tcase targetIndex:\n\t\tv := versions[targetIndex-1].String()\n\t\treturn v, v\n\tdefault:\n\t\tsort.Sort(version.Collection(versions))\n\t\treturn versions[targetIndex-1].String(), versions[l-1].String()\n\t}\n}","originalFile":"// Package metrics gather anonymous information about the usage of NetBird management\npackage metrics\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\tnbversion \"github.com/netbirdio/netbird/version\"\n)\n\nconst (\n\t// PayloadEvent identifies an event type\n\tPayloadEvent = \"self-hosted stats\"\n\t// payloadEndpoint metrics defaultEndpoint to send anonymous data\n\tpayloadEndpoint = \"https://metrics.netbird.io\"\n\t// defaultPushInterval default interval to push metrics\n\tdefaultPushInterval = 12 * time.Hour\n\t// requestTimeout http request timeout\n\trequestTimeout = 45 * time.Second\n\tEmbeddedType = \"embedded\"\n)\n\ntype getTokenResponse struct {\n\tPublicAPIToken string `json:\"public_api_token\"`\n}\n\ntype pushPayload struct {\n\tAPIKey string `json:\"api_key\"`\n\tDistinctID string `json:\"distinct_id\"`\n\tEvent string `json:\"event\"`\n\tProperties properties `json:\"properties\"`\n\tTimestamp time.Time `json:\"timestamp\"`\n}\n\n// properties metrics to push\ntype properties map[string]interface{}\n\n// DataSource metric data source\ntype DataSource interface {\n\tGetAllAccounts(ctx context.Context) []*types.Account\n\tGetStoreEngine() types.Engine\n\tGetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)\n}\n\n// ConnManager peer connection manager that holds state for current active connections\ntype ConnManager interface {\n\tGetAllConnectedPeers() map[string]struct{}\n}\n\n// Worker metrics collector and pusher\ntype Worker struct {\n\tctx context.Context\n\tid string\n\tidpManager string\n\tdataSource DataSource\n\tconnManager ConnManager\n\tstartupTime time.Time\n\tlastRun time.Time\n}\n\n// NewWorker returns a metrics worker\nfunc NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {\n\tcurrentTime := time.Now()\n\treturn &Worker{\n\t\tctx: ctx,\n\t\tid: id,\n\t\tidpManager: idpManager,\n\t\tdataSource: dataSource,\n\t\tconnManager: connManager,\n\t\tstartupTime: currentTime,\n\t\tlastRun: currentTime,\n\t}\n}\n\n// Run runs the metrics worker\nfunc (w *Worker) Run(ctx context.Context) {\n\tinterval := getMetricsInterval(ctx)\n\n\tpushTicker := time.NewTicker(interval)\n\tfor {\n\t\tselect {\n\t\tcase <-w.ctx.Done():\n\t\t\treturn\n\t\tcase <-pushTicker.C:\n\t\t\terr := w.sendMetrics(ctx)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithContext(ctx).Error(err)\n\t\t\t}\n\t\t\tw.lastRun = time.Now()\n\t\t}\n\t}\n}\n\nfunc getMetricsInterval(ctx context.Context) time.Duration {\n\tinterval := defaultPushInterval\n\tif os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") != \"\" {\n\t\tnewInterval, err := time.ParseDuration(os.Getenv(\"NETBIRD_METRICS_INTERVAL_IN_SECONDS\") + \"s\")\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"unable to parse NETBIRD_METRICS_INTERVAL_IN_SECONDS, using default interval %v. Error: %v\", defaultPushInterval, err)\n\t\t} else {\n\t\t\tlog.WithContext(ctx).Infof(\"using NETBIRD_METRICS_INTERVAL_IN_SECONDS %s\", newInterval)\n\t\t\tinterval = newInterval\n\t\t}\n\t}\n\treturn interval\n}\n\nfunc (w *Worker) sendMetrics(ctx context.Context) error {\n\tapiKey, err := getAPIKey(w.ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpayload := w.generatePayload(ctx, apiKey)\n\n\tpayloadString, err := buildMetricsPayload(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thttpClient := http.Client{}\n\n\texportJobReq, cancelCTX, err := createPostRequest(w.ctx, payloadEndpoint+\"/capture/\", payloadString)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create metrics post request %v\", err)\n\t}\n\tdefer cancelCTX()\n\n\tjobResp, err := httpClient.Do(exportJobReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to push metrics %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = jobResp.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing update metrics response body: %v\", err)\n\t\t}\n\t}()\n\n\tif jobResp.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"unable to push anonymous metrics, got statusCode %d\", jobResp.StatusCode)\n\t}\n\n\tlog.WithContext(ctx).Infof(\"sent anonymous metrics, next push will happen in %s. \"+\n\t\t\"You can disable these metrics by running with flag --disable-anonymous-metrics,\"+\n\t\t\" see more information at https://docs.netbird.io/about-netbird/faq#why-and-what-are-the-anonymous-usage-metrics\", getMetricsInterval(ctx))\n\n\treturn nil\n}\n\nfunc (w *Worker) generatePayload(ctx context.Context, apiKey string) pushPayload {\n\tproperties := w.generateProperties(ctx)\n\n\treturn pushPayload{\n\t\tAPIKey: apiKey,\n\t\tDistinctID: w.id,\n\t\tEvent: PayloadEvent,\n\t\tProperties: properties,\n\t\tTimestamp: time.Now(),\n\t}\n}\n\nfunc (w *Worker) generateProperties(ctx context.Context) properties {\n\tvar (\n\t\tuptime float64\n\t\taccounts int\n\t\texpirationEnabled int\n\t\tusers int\n\t\tserviceUsers int\n\t\tpats int\n\t\tpeers int\n\t\tpeersSSHEnabled int\n\t\tsetupKeysUsage int\n\t\tephemeralPeersSKs int\n\t\tephemeralPeersSKUsage int\n\t\tactivePeersLastDay int\n\t\tactiveUserPeersLastDay int\n\t\tosPeers map[string]int\n\t\tactiveUsersLastDay map[string]struct{}\n\t\tuserPeers int\n\t\trules int\n\t\trulesProtocol map[string]int\n\t\trulesDirection map[string]int\n\t\trulesWithSrcPostureChecks int\n\t\tpostureChecks int\n\t\tgroups int\n\t\troutes int\n\t\troutesWithRGGroups int\n\t\tnetworks int\n\t\tnetworkResources int\n\t\tnetworkRouters int\n\t\tnetworkRoutersWithPG int\n\t\tnameservers int\n\t\tuiClient int\n\t\tversion string\n\t\tpeerActiveVersions []string\n\t\tosUIClients map[string]int\n\t\trosenpassEnabled int\n\t\tlocalUsers int\n\t\tidpUsers int\n\t\tembeddedIdpTypes map[string]int\n\t\tservices int\n\t\tservicesEnabled int\n\t\tservicesTargets int\n\t\tservicesStatusActive int\n\t\tservicesStatusPending int\n\t\tservicesStatusError int\n\t\tservicesTargetType map[string]int\n\t\tservicesAuthPassword int\n\t\tservicesAuthPin int\n\t\tservicesAuthOIDC int\n\t)\n\tstart := time.Now()\n\tmetricsProperties := make(properties)\n\tosPeers = make(map[string]int)\n\tosUIClients = make(map[string]int)\n\trulesProtocol = make(map[string]int)\n\trulesDirection = make(map[string]int)\n\tactiveUsersLastDay = make(map[string]struct{})\n\tembeddedIdpTypes = make(map[string]int)\n\tservicesTargetType = make(map[string]int)\n\tuptime = time.Since(w.startupTime).Seconds()\n\tconnections := w.connManager.GetAllConnectedPeers()\n\tversion = nbversion.NetbirdVersion()\n\n\tcustomDomains, customDomainsValidated, _ := w.dataSource.GetCustomDomainsCounts(ctx)\n\n\tfor _, account := range w.dataSource.GetAllAccounts(ctx) {\n\t\taccounts++\n\n\t\tif account.Settings.PeerLoginExpirationEnabled {\n\t\t\texpirationEnabled++\n\t\t}\n\n\t\tgroups += len(account.Groups)\n\t\tnetworks += len(account.Networks)\n\t\tnetworkResources += len(account.NetworkResources)\n\n\t\tnetworkRouters += len(account.NetworkRouters)\n\t\tfor _, router := range account.NetworkRouters {\n\t\t\tif len(router.PeerGroups) > 0 {\n\t\t\t\tnetworkRoutersWithPG++\n\t\t\t}\n\t\t}\n\n\t\troutes += len(account.Routes)\n\t\tfor _, route := range account.Routes {\n\t\t\tif len(route.PeerGroups) > 0 {\n\t\t\t\troutesWithRGGroups++\n\t\t\t}\n\t\t}\n\t\tnameservers += len(account.NameServerGroups)\n\n\t\tfor _, policy := range account.Policies {\n\t\t\tfor _, rule := range policy.Rules {\n\t\t\t\trules++\n\t\t\t\trulesProtocol[string(rule.Protocol)]++\n\t\t\t\tif rule.Bidirectional {\n\t\t\t\t\trulesDirection[\"bidirectional\"]++\n\t\t\t\t} else {\n\t\t\t\t\trulesDirection[\"oneway\"]++\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(policy.SourcePostureChecks) > 0 {\n\t\t\t\trulesWithSrcPostureChecks++\n\t\t\t}\n\t\t}\n\n\t\tpostureChecks += len(account.PostureChecks)\n\n\t\tfor _, user := range account.Users {\n\t\t\tif user.IsServiceUser {\n\t\t\t\tserviceUsers++\n\t\t\t} else {\n\t\t\t\tusers++\n\t\t\t\tif w.idpManager == EmbeddedType {\n\t\t\t\t\t_, idpID, err := dex.DecodeDexUserID(user.Id)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif idpID == \"local\" {\n\t\t\t\t\t\t\tlocalUsers++\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tidpUsers++\n\t\t\t\t\t\t}\n\t\t\t\t\t\tidpType := extractIdpType(idpID)\n\t\t\t\t\t\tembeddedIdpTypes[idpType]++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpats += len(user.PATs)\n\t\t}\n\n\t\tfor _, key := range account.SetupKeys {\n\t\t\tsetupKeysUsage += key.UsedTimes\n\t\t\tif key.Ephemeral {\n\t\t\t\tephemeralPeersSKs++\n\t\t\t\tephemeralPeersSKUsage += key.UsedTimes\n\t\t\t}\n\t\t}\n\n\t\tfor _, peer := range account.Peers {\n\t\t\tpeers++\n\n\t\t\tif peer.SSHEnabled || peer.Meta.Flags.ServerSSHAllowed {\n\t\t\t\tpeersSSHEnabled++\n\t\t\t}\n\n\t\t\tif peer.Meta.Flags.RosenpassEnabled {\n\t\t\t\trosenpassEnabled++\n\t\t\t}\n\n\t\t\tif peer.UserID != \"\" {\n\t\t\t\tuserPeers++\n\t\t\t}\n\n\t\t\tosKey := strings.ToLower(fmt.Sprintf(\"peer_os_%s\", peer.Meta.GoOS))\n\t\t\tosCount := osPeers[osKey]\n\t\t\tosPeers[osKey] = osCount + 1\n\n\t\t\tif peer.Meta.UIVersion != \"\" {\n\t\t\t\tuiClient++\n\t\t\t\tuiOSKey := strings.ToLower(fmt.Sprintf(\"ui_client_os_%s\", peer.Meta.GoOS))\n\t\t\t\tosUICount := osUIClients[uiOSKey]\n\t\t\t\tosUIClients[uiOSKey] = osUICount + 1\n\t\t\t}\n\n\t\t\t_, connected := connections[peer.ID]\n\t\t\tif connected || peer.Status.LastSeen.After(w.lastRun) {\n\t\t\t\tactivePeersLastDay++\n\t\t\t\tif peer.UserID != \"\" {\n\t\t\t\t\tactiveUserPeersLastDay++\n\t\t\t\t\tactiveUsersLastDay[peer.UserID] = struct{}{}\n\t\t\t\t}\n\t\t\t\tosActiveKey := osKey + \"_active\"\n\t\t\t\tosActiveCount := osPeers[osActiveKey]\n\t\t\t\tosPeers[osActiveKey] = osActiveCount + 1\n\t\t\t\tpeerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)\n\t\t\t}\n\t\t}\n\n\t\tfor _, service := range account.Services {\n\t\t\tservices++\n\t\t\tif service.Enabled {\n\t\t\t\tservicesEnabled++\n\t\t\t}\n\t\t\tservicesTargets += len(service.Targets)\n\n\t\t\tswitch rpservice.Status(service.Meta.Status) {\n\t\t\tcase rpservice.StatusActive:\n\t\t\t\tservicesStatusActive++\n\t\t\tcase rpservice.StatusPending:\n\t\t\t\tservicesStatusPending++\n\t\t\tcase rpservice.StatusError, rpservice.StatusCertificateFailed, rpservice.StatusTunnelNotCreated:\n\t\t\t\tservicesStatusError++\n\t\t\t}\n\n\t\t\tfor _, target := range service.Targets {\n\t\t\t\tservicesTargetType[target.TargetType]++\n\t\t\t}\n\n\t\t\tif service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled {\n\t\t\t\tservicesAuthPassword++\n\t\t\t}\n\t\t\tif service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled {\n\t\t\t\tservicesAuthPin++\n\t\t\t}\n\t\t\tif service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled {\n\t\t\t\tservicesAuthOIDC++\n\t\t\t}\n\t\t}\n\t}\n\n\tminActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)\n\tmetricsProperties[\"uptime\"] = uptime\n\tmetricsProperties[\"accounts\"] = accounts\n\tmetricsProperties[\"users\"] = users\n\tmetricsProperties[\"service_users\"] = serviceUsers\n\tmetricsProperties[\"pats\"] = pats\n\tmetricsProperties[\"peers\"] = peers\n\tmetricsProperties[\"peers_ssh_enabled\"] = peersSSHEnabled\n\tmetricsProperties[\"peers_login_expiration_enabled\"] = expirationEnabled\n\tmetricsProperties[\"setup_keys_usage\"] = setupKeysUsage\n\tmetricsProperties[\"ephemeral_peers_setup_keys\"] = ephemeralPeersSKs\n\tmetricsProperties[\"ephemeral_peers_setup_keys_usage\"] = ephemeralPeersSKUsage\n\tmetricsProperties[\"active_peers_last_day\"] = activePeersLastDay\n\tmetricsProperties[\"active_user_peers_last_day\"] = activeUserPeersLastDay\n\tmetricsProperties[\"active_users_last_day\"] = len(activeUsersLastDay)\n\tmetricsProperties[\"user_peers\"] = userPeers\n\tmetricsProperties[\"rules\"] = rules\n\tmetricsProperties[\"rules_with_src_posture_checks\"] = rulesWithSrcPostureChecks\n\tmetricsProperties[\"posture_checks\"] = postureChecks\n\tmetricsProperties[\"groups\"] = groups\n\tmetricsProperties[\"networks\"] = networks\n\tmetricsProperties[\"network_resources\"] = networkResources\n\tmetricsProperties[\"network_routers\"] = networkRouters\n\tmetricsProperties[\"network_routers_with_groups\"] = networkRoutersWithPG\n\tmetricsProperties[\"routes\"] = routes\n\tmetricsProperties[\"routes_with_routing_groups\"] = routesWithRGGroups\n\tmetricsProperties[\"nameservers\"] = nameservers\n\tmetricsProperties[\"version\"] = version\n\tmetricsProperties[\"min_active_peer_version\"] = minActivePeerVersion\n\tmetricsProperties[\"max_active_peer_version\"] = maxActivePeerVersion\n\tmetricsProperties[\"ui_clients\"] = uiClient\n\tmetricsProperties[\"idp_manager\"] = w.idpManager\n\tmetricsProperties[\"store_engine\"] = w.dataSource.GetStoreEngine()\n\tmetricsProperties[\"rosenpass_enabled\"] = rosenpassEnabled\n\tmetricsProperties[\"local_users_count\"] = localUsers\n\tmetricsProperties[\"idp_users_count\"] = idpUsers\n\tmetricsProperties[\"embedded_idp_count\"] = len(embeddedIdpTypes)\n\n\tmetricsProperties[\"services\"] = services\n\tmetricsProperties[\"services_enabled\"] = servicesEnabled\n\tmetricsProperties[\"services_targets\"] = servicesTargets\n\tmetricsProperties[\"services_status_active\"] = servicesStatusActive\n\tmetricsProperties[\"services_status_pending\"] = servicesStatusPending\n\tmetricsProperties[\"services_status_error\"] = servicesStatusError\n\tmetricsProperties[\"services_auth_password\"] = servicesAuthPassword\n\tmetricsProperties[\"services_auth_pin\"] = servicesAuthPin\n\tmetricsProperties[\"services_auth_oidc\"] = servicesAuthOIDC\n\tmetricsProperties[\"custom_domains\"] = customDomains\n\tmetricsProperties[\"custom_domains_validated\"] = customDomainsValidated\n\n\tfor targetType, count := range servicesTargetType {\n\t\tmetricsProperties[\"services_target_type_\"+targetType] = count\n\t}\n\n\tfor idpType, count := range embeddedIdpTypes {\n\t\tmetricsProperties[\"embedded_idp_users_\"+idpType] = count\n\t}\n\n\tfor protocol, count := range rulesProtocol {\n\t\tmetricsProperties[\"rules_protocol_\"+protocol] = count\n\t}\n\n\tfor direction, count := range rulesDirection {\n\t\tmetricsProperties[\"rules_direction_\"+direction] = count\n\t}\n\n\tfor os, count := range osPeers {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tfor os, count := range osUIClients {\n\t\tmetricsProperties[os] = count\n\t}\n\n\tmetricsProperties[\"metric_generation_time\"] = time.Since(start).Milliseconds()\n\n\treturn metricsProperties\n}\n\nfunc getAPIKey(ctx context.Context) (string, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\tdefer cancel()\n\n\thttpClient := http.Client{}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+\"/GetToken\", nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create request for metrics public api token %v\", err)\n\t}\n\tresponse, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to request metrics public api token %v\", err)\n\t}\n\n\tdefer func() {\n\t\terr = response.Body.Close()\n\t\tif err != nil {\n\t\t\tlog.WithContext(ctx).Errorf(\"error while closing metrics token response body: %v\", err)\n\t\t}\n\t}()\n\n\tif response.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"unable to retrieve metrics token, statusCode %d\", response.StatusCode)\n\t}\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't get metrics token response; %v\", err)\n\t}\n\n\tvar tokenResponse getTokenResponse\n\n\terr = json.Unmarshal(body, &tokenResponse)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"coudln't parse metrics public api token; %v\", err)\n\t}\n\n\treturn tokenResponse.PublicAPIToken, nil\n}\n\nfunc buildMetricsPayload(payload pushPayload) (string, error) {\n\tstr, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to marshal metrics payload, got err: %v\", err)\n\t}\n\treturn string(str), nil\n}\n\nfunc createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, context.CancelFunc, error) {\n\tctx, cancel := context.WithTimeout(ctx, requestTimeout)\n\treqURL := endpoint\n\n\tpayload := strings.NewReader(payloadStr)\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", reqURL, payload)\n\tif err != nil {\n\t\tcancel()\n\t\treturn nil, nil, err\n\t}\n\treq.Header.Add(\"content-type\", \"application/json\")\n\n\treturn req, cancel, nil\n}\n\n// extractIdpType extracts the IdP type from a Dex connector ID.\n// Connector IDs are formatted as \" - \" (e.g., \"okta-abc123\", \"zitadel-xyz\").\n// Returns the type prefix, or \"oidc\" if no known prefix is found.\nfunc extractIdpType(connectorID string) string {\n\tif connectorID == \"\" {\n\t\treturn \"unknown\"\n\t}\n\tidx := strings.LastIndex(connectorID, \"-\")\n\tif idx <= 0 {\n\t\treturn strings.ToLower(connectorID)\n\t}\n\treturn strings.ToLower(connectorID[:idx])\n}\n\nfunc getMinMaxVersion(inputList []string) (string, string) {\n\tversions := make([]*version.Version, 0)\n\n\tfor _, raw := range inputList {\n\t\tif raw != \"\" && nbversion.SemverRegexp.MatchString(raw) {\n\t\t\tv, err := version.NewVersion(raw)\n\t\t\tif err == nil {\n\t\t\t\tversions = append(versions, v)\n\t\t\t}\n\t\t}\n\t}\n\n\ttargetIndex := 1\n\tl := len(versions)\n\n\tswitch l {\n\tcase 0:\n\t\treturn \"\", \"\"\n\tcase targetIndex:\n\t\tv := versions[targetIndex-1].String()\n\t\treturn v, v\n\tdefault:\n\t\tsort.Sort(version.Collection(versions))\n\t\treturn versions[targetIndex-1].String(), versions[l-1].String()\n\t}\n}\n","structuredPatch":[{"oldStart":531,"oldLines":12,"newStart":531,"newLines":12,"lines":[" // Connector IDs are formatted as \" - \" (e.g., \"okta-abc123\", \"zitadel-xyz\")."," // Returns the type prefix, or \"oidc\" if no known prefix is found."," func extractIdpType(connectorID string) string {","- if connectorID == \"\" {","- return \"unknown\"","+ if connectorID == \"local\" {","+ return \"local\""," }"," idx := strings.LastIndex(connectorID, \"-\")"," if idx <= 0 {","- return strings.ToLower(connectorID)","+ return \"oidc\""," }"," return strings.ToLower(connectorID[:idx])"," }"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"840055ea-c017-4d7c-9183-a832ab1f67a0"} +{"parentUuid":"ae9bd03c-6a12-4012-9cf8-2586c73953b6","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01UJ4HsqY9JB3pKsJkiUvrJs","type":"message","role":"assistant","content":[{"type":"text","text":"Update the test expectations:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":405,"cache_read_input_tokens":126091,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":405},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhqodquAzEn4iXnLQdZW","type":"assistant","uuid":"2d060ce2-c480-4244-bcfd-abb159c04fc3","timestamp":"2026-03-04T09:58:02.543Z"} +{"parentUuid":"2d060ce2-c480-4244-bcfd-abb159c04fc3","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01UJ4HsqY9JB3pKsJkiUvrJs","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01EPhYjr9zL61mNQkMsXyhte","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","old_string":"\t\t{\"local\", \"local\"},\n\t\t{\"\", \"unknown\"},","new_string":"\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":405,"cache_read_input_tokens":126091,"output_tokens":199,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":405,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqodquAzEn4iXnLQdZW","type":"assistant","uuid":"f046e010-b079-4c77-9e2a-3f9677ab14b7","timestamp":"2026-03-04T09:58:04.070Z"} +{"parentUuid":"f046e010-b079-4c77-9e2a-3f9677ab14b7","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_01EPhYjr9zL61mNQkMsXyhte","toolUseID":"toolu_01EPhYjr9zL61mNQkMsXyhte","timestamp":"2026-03-04T09:58:16.952Z","uuid":"6b01aca3-ec21-4024-bfa6-1b1e34f76a06"} +{"parentUuid":"f046e010-b079-4c77-9e2a-3f9677ab14b7","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01EPhYjr9zL61mNQkMsXyhte","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go has been updated successfully."}]},"uuid":"53177745-c151-41f0-8607-a63e40f762ce","timestamp":"2026-03-04T09:58:16.951Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","oldString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"\", \"unknown\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","newString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","originalFile":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"\", \"unknown\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n","structuredPatch":[{"oldStart":440,"oldLines":7,"newStart":440,"newLines":8,"lines":[" {\"authentik-abc123\", \"authentik\"},"," {\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},"," {\"local\", \"local\"},","- {\"\", \"unknown\"},","+ {\"d6jvvp69kmnc73c9pl40\", \"oidc\"},","+ {\"\", \"oidc\"},"," }"," "," for _, tt := range tests {"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"f046e010-b079-4c77-9e2a-3f9677ab14b7"} +{"parentUuid":"53177745-c151-41f0-8607-a63e40f762ce","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01776mfUpM5CDJjumL7qbx8c","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01S42rDcRTsXjreernqUH3pT","name":"Bash","input":{"command":"go test ./management/server/metrics/... -run 'TestExtractIdpType|TestGenerateProperties' -v 2>&1","description":"Run metrics tests","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":249,"cache_read_input_tokens":126496,"output_tokens":120,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":249,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqq9AYYSyKCJJ9b6xhu","type":"assistant","uuid":"0dfcb339-d208-4b00-b1eb-8d681ecc30dc","timestamp":"2026-03-04T09:58:22.759Z"} +{"parentUuid":"0dfcb339-d208-4b00-b1eb-8d681ecc30dc","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":3,"totalLines":0,"totalBytes":0,"taskId":"bikpovgcn","timeoutMs":120000},"toolUseID":"bash-progress-0","parentToolUseID":"toolu_01S42rDcRTsXjreernqUH3pT","uuid":"e69919de-d33e-462f-a98b-ddc3c3b715d2","timestamp":"2026-03-04T09:58:25.861Z"} +{"parentUuid":"e69919de-d33e-462f-a98b-ddc3c3b715d2","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":4,"totalLines":0,"totalBytes":0,"taskId":"bikpovgcn","timeoutMs":120000},"toolUseID":"bash-progress-1","parentToolUseID":"toolu_01S42rDcRTsXjreernqUH3pT","uuid":"d310578f-9ebe-48b5-bf5f-86db2f3dac48","timestamp":"2026-03-04T09:58:26.859Z"} +{"parentUuid":"0dfcb339-d208-4b00-b1eb-8d681ecc30dc","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01S42rDcRTsXjreernqUH3pT","type":"tool_result","content":"=== RUN TestGenerateProperties\n--- PASS: TestGenerateProperties (0.00s)\n=== RUN TestExtractIdpType\n=== RUN TestExtractIdpType/okta-abc123def\n=== RUN REDACTED\n=== RUN TestExtractIdpType/entra-xyz789\n=== RUN TestExtractIdpType/google-abc123\n=== RUN TestExtractIdpType/pocketid-abc123\n=== RUN TestExtractIdpType/microsoft-abc123\n=== RUN TestExtractIdpType/authentik-abc123\n=== RUN REDACTED\n=== RUN TestExtractIdpType/local\n=== RUN REDACTED\n=== RUN TestExtractIdpType/#00\n--- PASS: TestExtractIdpType (0.00s)\n --- PASS: TestExtractIdpType/okta-abc123def (0.00s)\n --- PASS: REDACTED (0.00s)\n --- PASS: TestExtractIdpType/entra-xyz789 (0.00s)\n --- PASS: TestExtractIdpType/google-abc123 (0.00s)\n --- PASS: TestExtractIdpType/pocketid-abc123 (0.00s)\n --- PASS: TestExtractIdpType/microsoft-abc123 (0.00s)\n --- PASS: TestExtractIdpType/authentik-abc123 (0.00s)\n --- PASS: REDACTED (0.00s)\n --- PASS: TestExtractIdpType/local (0.00s)\n --- PASS: REDACTED (0.00s)\n --- PASS: TestExtractIdpType/#00 (0.00s)\nPASS\nok \tgithub.com/netbirdio/netbird/management/server/metrics\t0.825s","is_error":false}]},"uuid":"2ea3b9ad-68f4-424f-8ac0-4c2e79b88f1d","timestamp":"2026-03-04T09:58:27.441Z","toolUseResult":{"stdout":"=== RUN TestGenerateProperties\n--- PASS: TestGenerateProperties (0.00s)\n=== RUN TestExtractIdpType\n=== RUN TestExtractIdpType/okta-abc123def\n=== RUN REDACTED\n=== RUN TestExtractIdpType/entra-xyz789\n=== RUN TestExtractIdpType/google-abc123\n=== RUN TestExtractIdpType/pocketid-abc123\n=== RUN TestExtractIdpType/microsoft-abc123\n=== RUN TestExtractIdpType/authentik-abc123\n=== RUN REDACTED\n=== RUN TestExtractIdpType/local\n=== RUN REDACTED\n=== RUN TestExtractIdpType/#00\n--- PASS: TestExtractIdpType (0.00s)\n --- PASS: TestExtractIdpType/okta-abc123def (0.00s)\n --- PASS: REDACTED (0.00s)\n --- PASS: TestExtractIdpType/entra-xyz789 (0.00s)\n --- PASS: TestExtractIdpType/google-abc123 (0.00s)\n --- PASS: TestExtractIdpType/pocketid-abc123 (0.00s)\n --- PASS: TestExtractIdpType/microsoft-abc123 (0.00s)\n --- PASS: TestExtractIdpType/authentik-abc123 (0.00s)\n --- PASS: REDACTED (0.00s)\n --- PASS: TestExtractIdpType/local (0.00s)\n --- PASS: REDACTED (0.00s)\n --- PASS: TestExtractIdpType/#00 (0.00s)\nPASS\nok \tgithub.com/netbirdio/netbird/management/server/metrics\t0.825s","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"0dfcb339-d208-4b00-b1eb-8d681ecc30dc"} +{"parentUuid":"2ea3b9ad-68f4-424f-8ac0-4c2e79b88f1d","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01UAfLy5C7ftuu51qqHs1PrQ","type":"message","role":"assistant","content":[{"type":"text","text":"All pass. Now `extractIdpType` correctly handles all three cases:\n\n- `\"local\"` → `\"local\"` (embedded local users)\n- `\"d6jvvp69kmnc73c9pl40\"` (no dash, raw xid) → `\"oidc\"` (generic OIDC)\n- `\"authentik-abc123\"` (prefixed) → `\"authentik\"` (known provider)"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":750,"cache_read_input_tokens":126745,"output_tokens":107,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":750,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqqqh6eK2jsfoPuMeyv","type":"assistant","uuid":"e691a25e-6e13-4404-872a-f244e76c69b2","timestamp":"2026-03-04T09:58:33.299Z"} +{"parentUuid":"e691a25e-6e13-4404-872a-f244e76c69b2","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"Stop","hookName":"Stop","command":"entire hooks claude-code stop"},"parentToolUseID":"27325602-203a-49e1-b987-9a6d29721bfb","toolUseID":"27325602-203a-49e1-b987-9a6d29721bfb","timestamp":"2026-03-04T09:58:33.384Z","uuid":"2797a5b9-3ed9-40b8-ad44-cb2780991ad7"} +{"parentUuid":"2797a5b9-3ed9-40b8-ad44-cb2780991ad7","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"system","subtype":"stop_hook_summary","hookCount":1,"hookInfos":[{"command":"entire hooks claude-code stop","durationMs":1075}],"hookErrors":[],"preventedContinuation":false,"stopReason":"","hasOutput":true,"level":"suggestion","timestamp":"2026-03-04T09:58:34.460Z","uuid":"7e12ac1e-6798-4dc4-9b46-1e88577f5f3d","toolUseID":"27325602-203a-49e1-b987-9a6d29721bfb"} +{"parentUuid":"7e12ac1e-6798-4dc4-9b46-1e88577f5f3d","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"system","subtype":"turn_duration","durationMs":39788,"timestamp":"2026-03-04T09:58:34.461Z","uuid":"ad846ac7-ac1a-4757-b23a-d04658724e74","isMeta":false} +{"type":"file-history-snapshot","messageId":"3101a310-53da-40b7-97c7-505b3526f9bf","snapshot":{"messageId":"3101a310-53da-40b7-97c7-505b3526f9bf","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v3","version":3,"backupTime":"2026-03-03T09:45:34.548Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v3","version":3,"backupTime":"2026-03-04T09:35:30.335Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v2","version":2,"backupTime":"2026-03-02T19:45:28.129Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/config.yaml.example":{"backupFileName":"ab8acefcb43dda84@v2","version":2,"backupTime":"2026-03-02T19:45:28.132Z"},"management/server/metrics/selfhosted.go":{"backupFileName":"e4e6cf8f05310807@v3","version":3,"backupTime":"2026-03-04T09:59:31.749Z"},"management/server/metrics/selfhosted_test.go":{"backupFileName":"a0ee876802b49262@v3","version":3,"backupTime":"2026-03-04T09:59:31.750Z"}},"timestamp":"2026-03-04T09:59:31.746Z"},"isSnapshotUpdate":false} +{"parentUuid":"ad846ac7-ac1a-4757-b23a-d04658724e74","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":"you need to test generateProperties() with real dex-encoded userids too"},"uuid":"3101a310-53da-40b7-97c7-505b3526f9bf","timestamp":"2026-03-04T09:59:30.038Z","permissionMode":"default"} +{"parentUuid":"3101a310-53da-40b7-97c7-505b3526f9bf","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01Wk7Zi2DkKrc6jComw4ATcw","type":"message","role":"assistant","content":[{"type":"text","text":"\n\nGood point. The existing test uses `dex.EncodeDexUserID` for local and zitadel, but doesn't cover generic OIDC (no prefix). Let me check what's there and add it."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":200,"cache_read_input_tokens":127600,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":200},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhqvbRzLLJrRUQTZRR7b","type":"assistant","uuid":"f3d7b164-909f-400c-a3ea-e55c901290bb","timestamp":"2026-03-04T09:59:37.607Z"} +{"parentUuid":"f3d7b164-909f-400c-a3ea-e55c901290bb","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01Wk7Zi2DkKrc6jComw4ATcw","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01ERiJcfwouNcfHDrRfhEGSa","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","offset":29,"limit":30},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":200,"cache_read_input_tokens":127600,"output_tokens":169,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":200,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqvbRzLLJrRUQTZRR7b","type":"assistant","uuid":"892f9964-0499-4f88-9f85-ba1f2a05e675","timestamp":"2026-03-04T09:59:41.998Z"} +{"parentUuid":"892f9964-0499-4f88-9f85-ba1f2a05e675","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01ERiJcfwouNcfHDrRfhEGSa","toolUseID":"toolu_01ERiJcfwouNcfHDrRfhEGSa","timestamp":"2026-03-04T09:59:42.185Z","uuid":"a7098c16-dce6-4284-a3cf-6855e6b05e32"} +{"parentUuid":"892f9964-0499-4f88-9f85-ba1f2a05e675","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01ERiJcfwouNcfHDrRfhEGSa","type":"tool_result","content":" 29→func (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n 30→\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n 31→\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n 32→\treturn []*types.Account{\n 33→\t\t{\n 34→\t\t\tId: \"1\",\n 35→\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n 36→\t\t\tSetupKeys: map[string]*types.SetupKey{\n 37→\t\t\t\t\"1\": {\n 38→\t\t\t\t\tId: \"1\",\n 39→\t\t\t\t\tEphemeral: true,\n 40→\t\t\t\t\tUsedTimes: 1,\n 41→\t\t\t\t},\n 42→\t\t\t},\n 43→\t\t\tGroups: map[string]*types.Group{\n 44→\t\t\t\t\"1\": {},\n 45→\t\t\t\t\"2\": {},\n 46→\t\t\t},\n 47→\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n 48→\t\t\t\t\"1\": {},\n 49→\t\t\t},\n 50→\t\t\tPeers: map[string]*nbpeer.Peer{\n 51→\t\t\t\t\"1\": {\n 52→\t\t\t\t\tID: \"1\",\n 53→\t\t\t\t\tUserID: \"test\",\n 54→\t\t\t\t\tSSHEnabled: false,\n 55→\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n 56→\t\t\t\t},\n 57→\t\t\t},\n 58→\t\t\tPolicies: []*types.Policy{"}]},"uuid":"ea844598-b24d-46ed-8edd-f4f394ef875a","timestamp":"2026-03-04T09:59:42.184Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","content":"func (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{","numLines":30,"startLine":29,"totalLines":456}},"sourceToolAssistantUUID":"892f9964-0499-4f88-9f85-ba1f2a05e675"} +{"parentUuid":"ea844598-b24d-46ed-8edd-f4f394ef875a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_012gvHP2YQWePXPfJpw3tk8w","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01FJMBDwNVYX9pRDg4w9fcAC","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","offset":100,"limit":50},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":633,"cache_read_input_tokens":127800,"output_tokens":118,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":633,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqwip2Mc1Ct6qpFNz8M","type":"assistant","uuid":"d1ab9d3f-8480-4182-a269-fbc521e6bfad","timestamp":"2026-03-04T09:59:51.890Z"} +{"parentUuid":"d1ab9d3f-8480-4182-a269-fbc521e6bfad","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01FJMBDwNVYX9pRDg4w9fcAC","toolUseID":"toolu_01FJMBDwNVYX9pRDg4w9fcAC","timestamp":"2026-03-04T09:59:51.978Z","uuid":"4eb36edf-1a6d-46ef-aa1f-2236e56d98af"} +{"parentUuid":"d1ab9d3f-8480-4182-a269-fbc521e6bfad","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01FJMBDwNVYX9pRDg4w9fcAC","type":"tool_result","content":" 100→\t\t\t\t\t},\n 101→\t\t\t\t},\n 102→\t\t\t},\n 103→\t\t\tUsers: map[string]*types.User{\n 104→\t\t\t\t\"1\": {\n 105→\t\t\t\t\tId: \"1\",\n 106→\t\t\t\t\tIsServiceUser: true,\n 107→\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n 108→\t\t\t\t\t\t\"1\": {},\n 109→\t\t\t\t\t},\n 110→\t\t\t\t},\n 111→\t\t\t\tlocalUserID: {\n 112→\t\t\t\t\tId: localUserID,\n 113→\t\t\t\t\tIsServiceUser: false,\n 114→\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n 115→\t\t\t\t\t\t\"1\": {},\n 116→\t\t\t\t\t},\n 117→\t\t\t\t},\n 118→\t\t\t},\n 119→\t\t\tServices: []*rpservice.Service{\n 120→\t\t\t\t{\n 121→\t\t\t\t\tID: \"svc1\",\n 122→\t\t\t\t\tEnabled: true,\n 123→\t\t\t\t\tTargets: []*rpservice.Target{\n 124→\t\t\t\t\t\t{TargetType: \"peer\"},\n 125→\t\t\t\t\t\t{TargetType: \"host\"},\n 126→\t\t\t\t\t},\n 127→\t\t\t\t\tAuth: rpservice.AuthConfig{\n 128→\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n 129→\t\t\t\t\t},\n 130→\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n 131→\t\t\t\t},\n 132→\t\t\t\t{\n 133→\t\t\t\t\tID: \"svc2\",\n 134→\t\t\t\t\tEnabled: false,\n 135→\t\t\t\t\tTargets: []*rpservice.Target{\n 136→\t\t\t\t\t\t{TargetType: \"domain\"},\n 137→\t\t\t\t\t},\n 138→\t\t\t\t\tAuth: rpservice.AuthConfig{\n 139→\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n 140→\t\t\t\t\t},\n 141→\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n 142→\t\t\t\t},\n 143→\t\t\t},\n 144→\t\t},\n 145→\t\t{\n 146→\t\t\tId: \"2\",\n 147→\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n 148→\t\t\tSetupKeys: map[string]*types.SetupKey{\n 149→\t\t\t\t\"1\": {"}]},"uuid":"733187d3-7567-4cd0-b3fc-2fe4ff10f619","timestamp":"2026-03-04T09:59:51.978Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","content":"\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {","numLines":50,"startLine":100,"totalLines":456}},"sourceToolAssistantUUID":"d1ab9d3f-8480-4182-a269-fbc521e6bfad"} +{"parentUuid":"733187d3-7567-4cd0-b3fc-2fe4ff10f619","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01HxXQMigTRKMc8eABtpQtEj","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_015zAMaCqmat25Q4BsHUweco","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","offset":190,"limit":70},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":692,"cache_read_input_tokens":128433,"output_tokens":118,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":692,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqx65wqVXgxo5RJigWZ","type":"assistant","uuid":"f2e536eb-fa6f-4cdd-b770-5fd5ba63acce","timestamp":"2026-03-04T09:59:56.840Z"} +{"parentUuid":"f2e536eb-fa6f-4cdd-b770-5fd5ba63acce","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_015zAMaCqmat25Q4BsHUweco","toolUseID":"toolu_015zAMaCqmat25Q4BsHUweco","timestamp":"2026-03-04T09:59:56.980Z","uuid":"43b60da2-d962-4535-801b-59aafc7a8fb9"} +{"parentUuid":"f2e536eb-fa6f-4cdd-b770-5fd5ba63acce","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_015zAMaCqmat25Q4BsHUweco","type":"tool_result","content":" 190→\t\t\t\t\tID: \"1\",\n 191→\t\t\t\t\tPeerGroups: make([]string, 1),\n 192→\t\t\t\t},\n 193→\t\t\t},\n 194→\t\t\tUsers: map[string]*types.User{\n 195→\t\t\t\t\"1\": {\n 196→\t\t\t\t\tId: \"1\",\n 197→\t\t\t\t\tIsServiceUser: true,\n 198→\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n 199→\t\t\t\t\t\t\"1\": {},\n 200→\t\t\t\t\t},\n 201→\t\t\t\t},\n 202→\t\t\t\tidpUserID: {\n 203→\t\t\t\t\tId: idpUserID,\n 204→\t\t\t\t\tIsServiceUser: false,\n 205→\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n 206→\t\t\t\t\t\t\"1\": {},\n 207→\t\t\t\t\t},\n 208→\t\t\t\t},\n 209→\t\t\t},\n 210→\t\t\tNetworks: []*networkTypes.Network{\n 211→\t\t\t\t{\n 212→\t\t\t\t\tID: \"1\",\n 213→\t\t\t\t\tAccountID: \"1\",\n 214→\t\t\t\t},\n 215→\t\t\t},\n 216→\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n 217→\t\t\t\t{\n 218→\t\t\t\t\tID: \"1\",\n 219→\t\t\t\t\tAccountID: \"1\",\n 220→\t\t\t\t\tNetworkID: \"1\",\n 221→\t\t\t\t},\n 222→\t\t\t\t{\n 223→\t\t\t\t\tID: \"2\",\n 224→\t\t\t\t\tAccountID: \"1\",\n 225→\t\t\t\t\tNetworkID: \"1\",\n 226→\t\t\t\t},\n 227→\t\t\t},\n 228→\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n 229→\t\t\t\t{\n 230→\t\t\t\t\tID: \"1\",\n 231→\t\t\t\t\tAccountID: \"1\",\n 232→\t\t\t\t\tNetworkID: \"1\",\n 233→\t\t\t\t},\n 234→\t\t\t},\n 235→\t\t},\n 236→\t}\n 237→}\n 238→\n 239→// GetStoreEngine returns FileStoreEngine\n 240→func (mockDatasource) GetStoreEngine() types.Engine {\n 241→\treturn types.FileStoreEngine\n 242→}\n 243→\n 244→// GetCustomDomainsCounts returns test custom domain counts.\n 245→func (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n 246→\treturn 3, 2, nil\n 247→}\n 248→\n 249→// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\n 250→func TestGenerateProperties(t *testing.T) {\n 251→\tds := mockDatasource{}\n 252→\tworker := Worker{\n 253→\t\tdataSource: ds,\n 254→\t\tconnManager: ds,\n 255→\t\tidpManager: EmbeddedType,\n 256→\t}\n 257→\n 258→\tproperties := worker.generateProperties(context.Background())\n 259→"}]},"uuid":"42798a04-7f7f-4bcc-b94c-ff94db56e153","timestamp":"2026-03-04T09:59:56.980Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","content":"\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n","numLines":70,"startLine":190,"totalLines":456}},"sourceToolAssistantUUID":"f2e536eb-fa6f-4cdd-b770-5fd5ba63acce"} +{"parentUuid":"42798a04-7f7f-4bcc-b94c-ff94db56e153","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01RKogqg2DgJKkqG62WDUenV","type":"message","role":"assistant","content":[{"type":"text","text":"Now I see the full picture. Account 1 has 1 local user, account 2 has 1 zitadel user. I need to add a generic OIDC user (no prefix connector). I'll add it to account 2 alongside the zitadel user."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":855,"cache_read_input_tokens":129125,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":855},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhqxoM9wBUHZQHM2uzF6","type":"assistant","uuid":"fd0f8e1e-c77a-464f-8d06-892d5503dc53","timestamp":"2026-03-04T10:00:06.570Z"} +{"parentUuid":"fd0f8e1e-c77a-464f-8d06-892d5503dc53","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01RKogqg2DgJKkqG62WDUenV","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01AZPjZTCaMbZwtDFMeX7cHv","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","old_string":"\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")","new_string":"\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\toidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":855,"cache_read_input_tokens":129125,"output_tokens":366,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":855,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqxoM9wBUHZQHM2uzF6","type":"assistant","uuid":"0ac959a8-76ba-474c-9531-f83a9550efc2","timestamp":"2026-03-04T10:00:08.714Z"} +{"parentUuid":"0ac959a8-76ba-474c-9531-f83a9550efc2","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_01AZPjZTCaMbZwtDFMeX7cHv","toolUseID":"toolu_01AZPjZTCaMbZwtDFMeX7cHv","timestamp":"2026-03-04T10:00:21.952Z","uuid":"3166742e-85c1-46b4-a048-dd9785b89bf9"} +{"parentUuid":"0ac959a8-76ba-474c-9531-f83a9550efc2","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01AZPjZTCaMbZwtDFMeX7cHv","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go has been updated successfully."}]},"uuid":"0025a0db-da92-41b0-bd3d-b8f26c3b71cc","timestamp":"2026-03-04T10:00:21.951Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","oldString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","newString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\toidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","originalFile":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n","structuredPatch":[{"oldStart":29,"oldLines":6,"newStart":29,"newLines":7,"lines":[" func (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {"," localUserID := dex.EncodeDexUserID(\"10\", \"local\")"," idpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")","+ oidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")"," return []*types.Account{"," {"," Id: \"1\","]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"0ac959a8-76ba-474c-9531-f83a9550efc2"} +{"parentUuid":"0025a0db-da92-41b0-bd3d-b8f26c3b71cc","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01CYw36m6Xe2CEurKq3Bdi42","type":"message","role":"assistant","content":[{"type":"text","text":"Add the OIDC user to account 2's Users map:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":416,"cache_read_input_tokens":129980,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":416},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhqzHueZsneTyZkoMMU7","type":"assistant","uuid":"66ce78ab-a692-4c7a-9fe4-9a158aa8d968","timestamp":"2026-03-04T10:00:26.212Z"} +{"parentUuid":"66ce78ab-a692-4c7a-9fe4-9a158aa8d968","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01CYw36m6Xe2CEurKq3Bdi42","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01HdKdeUvJePuT2PGf587yyZ","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","old_string":"\t\t\tidpUserID: {\n\t\t\t\tId: idpUserID,\n\t\t\t\tIsServiceUser: false,\n\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\"1\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tNetworks: []*networkTypes.Network{","new_string":"\t\t\tidpUserID: {\n\t\t\t\tId: idpUserID,\n\t\t\t\tIsServiceUser: false,\n\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\"1\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t\toidcUserID: {\n\t\t\t\tId: oidcUserID,\n\t\t\t\tIsServiceUser: false,\n\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\"1\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tNetworks: []*networkTypes.Network{"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":416,"cache_read_input_tokens":129980,"output_tokens":345,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":416,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqzHueZsneTyZkoMMU7","type":"assistant","uuid":"3c915fe5-fb1d-42f7-a137-3926e442191b","timestamp":"2026-03-04T10:00:28.499Z"} +{"parentUuid":"3c915fe5-fb1d-42f7-a137-3926e442191b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"type":"tool_result","content":" String to replace not found in file.\nString: \t\t\tidpUserID: {\n\t\t\t\tId: idpUserID,\n\t\t\t\tIsServiceUser: false,\n\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\"1\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tNetworks: []*networkTypes.Network{ ","is_error":true,"tool_use_id":"toolu_01HdKdeUvJePuT2PGf587yyZ"}]},"uuid":"d590f62d-dfec-4768-a9da-70405647cf6e","timestamp":"2026-03-04T10:00:28.569Z","toolUseResult":"Error: String to replace not found in file.\nString: \t\t\tidpUserID: {\n\t\t\t\tId: idpUserID,\n\t\t\t\tIsServiceUser: false,\n\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\"1\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tNetworks: []*networkTypes.Network{","sourceToolAssistantUUID":"3c915fe5-fb1d-42f7-a137-3926e442191b"} +{"parentUuid":"d590f62d-dfec-4768-a9da-70405647cf6e","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01AKwaUVjGvHF2Tfog8SiVC7","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01BzzXz2K9jPJqDpAZqhZQtm","name":"Bash","input":{"command":"awk 'NR>=203 && NR<=212' /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go | cat -v","description":"Show exact content around the edit target"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":456,"cache_read_input_tokens":130396,"output_tokens":125,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":456,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhqznT8eU8mjJpVzvuSm","type":"assistant","uuid":"26589b64-3163-41b1-b2e1-467c37be618a","timestamp":"2026-03-04T10:00:33.691Z"} +{"parentUuid":"26589b64-3163-41b1-b2e1-467c37be618a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01BzzXz2K9jPJqDpAZqhZQtm","type":"tool_result","content":"\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{","is_error":false}]},"uuid":"10a21a3b-8122-46bd-933d-9e78b0a5fdf4","timestamp":"2026-03-04T10:03:21.527Z","toolUseResult":{"stdout":"\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"26589b64-3163-41b1-b2e1-467c37be618a"} +{"parentUuid":"10a21a3b-8122-46bd-933d-9e78b0a5fdf4","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01XdchM7ymyi1B7WkA1VmX7c","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01UCjyGYrUCdceDTr3ZeLqtK","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","old_string":"\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{","new_string":"\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toidcUserID: {\n\t\t\t\t\tId: oidcUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":207,"cache_read_input_tokens":130852,"output_tokens":329,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":207,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhrDY6o5AfgXLes5xaX2","type":"assistant","uuid":"d04d4531-1bb4-4018-8622-9d3952c73eff","timestamp":"2026-03-04T10:03:28.820Z"} +{"parentUuid":"d04d4531-1bb4-4018-8622-9d3952c73eff","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_01UCjyGYrUCdceDTr3ZeLqtK","toolUseID":"toolu_01UCjyGYrUCdceDTr3ZeLqtK","timestamp":"2026-03-04T10:04:08.203Z","uuid":"0ef3c0ef-1a60-4d3d-a846-fd5df88cc2b5"} +{"parentUuid":"d04d4531-1bb4-4018-8622-9d3952c73eff","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01UCjyGYrUCdceDTr3ZeLqtK","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go has been updated successfully."}]},"uuid":"e6dd3ea0-3f34-4d83-af0a-609009b683ca","timestamp":"2026-03-04T10:04:08.201Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","oldString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\toidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","newString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\toidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toidcUserID: {\n\t\t\t\t\tId: oidcUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","originalFile":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\toidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n","structuredPatch":[{"oldStart":207,"oldLines":6,"newStart":207,"newLines":13,"lines":[" \"1\": {},"," },"," },","+ oidcUserID: {","+ Id: oidcUserID,","+ IsServiceUser: false,","+ PATs: map[string]*types.PersonalAccessToken{","+ \"1\": {},","+ },","+ },"," },"," Networks: []*networkTypes.Network{"," {"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"d04d4531-1bb4-4018-8622-9d3952c73eff"} +{"parentUuid":"e6dd3ea0-3f34-4d83-af0a-609009b683ca","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01Qwh59KEFNxhhAd8Uc4oQxt","type":"message","role":"assistant","content":[{"type":"text","text":"Now update the test assertions to account for the new user (1 more user, 1 more PAT, 1 more idp user, 1 more embedded idp type, and the oidc metric):"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":490,"cache_read_input_tokens":131059,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":490},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhrGy15hVhsWYtVyGaAd","type":"assistant","uuid":"6bb2922d-2046-4de3-a2f3-0b70558ee93a","timestamp":"2026-03-04T10:04:14.134Z"} +{"parentUuid":"6bb2922d-2046-4de3-a2f3-0b70558ee93a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01Qwh59KEFNxhhAd8Uc4oQxt","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01BCDiPMtQPAPMja2U43pLoX","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","offset":281,"limit":30},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":490,"cache_read_input_tokens":131059,"output_tokens":167,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":490,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhrGy15hVhsWYtVyGaAd","type":"assistant","uuid":"b0a346aa-c18d-418b-9a34-5923ce9b516a","timestamp":"2026-03-04T10:04:14.947Z"} +{"parentUuid":"b0a346aa-c18d-418b-9a34-5923ce9b516a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01BCDiPMtQPAPMja2U43pLoX","toolUseID":"toolu_01BCDiPMtQPAPMja2U43pLoX","timestamp":"2026-03-04T10:04:15.170Z","uuid":"f1e81f25-e397-4bff-b3c7-936d4afef99c"} +{"parentUuid":"b0a346aa-c18d-418b-9a34-5923ce9b516a","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01BCDiPMtQPAPMja2U43pLoX","type":"tool_result","content":" 281→\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n 282→\t}\n 283→\tif properties[\"network_routers\"] != 1 {\n 284→\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n 285→\t}\n 286→\tif properties[\"rules\"] != 4 {\n 287→\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n 288→\t}\n 289→\tif properties[\"users\"] != 2 {\n 290→\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n 291→\t}\n 292→\tif properties[\"setup_keys_usage\"] != 2 {\n 293→\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n 294→\t}\n 295→\tif properties[\"pats\"] != 4 {\n 296→\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n 297→\t}\n 298→\tif properties[\"peers_ssh_enabled\"] != 2 {\n 299→\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n 300→\t}\n 301→\tif properties[\"routes_with_routing_groups\"] != 2 {\n 302→\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n 303→\t}\n 304→\tif properties[\"rules_protocol_tcp\"] != 4 {\n 305→\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n 306→\t}\n 307→\tif properties[\"rules_direction_oneway\"] != 2 {\n 308→\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n 309→\t}\n 310→"}]},"uuid":"dfd54a0e-afdb-4836-b2c1-583c2c6a353c","timestamp":"2026-03-04T10:04:15.169Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","content":"\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n","numLines":30,"startLine":281,"totalLines":464}},"sourceToolAssistantUUID":"b0a346aa-c18d-418b-9a34-5923ce9b516a"} +{"parentUuid":"dfd54a0e-afdb-4836-b2c1-583c2c6a353c","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_015G2e2LPANSPHj82v9BuLpA","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01ASFCdDLBiVJVupLiP8StSh","name":"Read","input":{"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","offset":370,"limit":30},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":715,"cache_read_input_tokens":131549,"output_tokens":118,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":715,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhrHX4tkdcuuv5nN3SU3","type":"assistant","uuid":"874c8648-9090-4f7b-957f-21259767cee4","timestamp":"2026-03-04T10:04:22.426Z"} +{"parentUuid":"874c8648-9090-4f7b-957f-21259767cee4","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read","command":"callback"},"parentToolUseID":"toolu_01ASFCdDLBiVJVupLiP8StSh","toolUseID":"toolu_01ASFCdDLBiVJVupLiP8StSh","timestamp":"2026-03-04T10:04:22.583Z","uuid":"63c0ddd5-f2e2-48c0-ae66-6bd857b6b51e"} +{"parentUuid":"874c8648-9090-4f7b-957f-21259767cee4","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01ASFCdDLBiVJVupLiP8StSh","type":"tool_result","content":" 370→\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n 371→\t}\n 372→\n 373→\tif properties[\"active_users_last_day\"] != 1 {\n 374→\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n 375→\t}\n 376→\n 377→\tif properties[\"local_users_count\"] != 1 {\n 378→\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n 379→\t}\n 380→\tif properties[\"idp_users_count\"] != 1 {\n 381→\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n 382→\t}\n 383→\tif properties[\"embedded_idp_users_local\"] != 1 {\n 384→\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n 385→\t}\n 386→\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n 387→\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n 388→\t}\n 389→\tif properties[\"embedded_idp_count\"] != 2 {\n 390→\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n 391→\t}\n 392→\n 393→\tif properties[\"services\"] != 2 {\n 394→\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n 395→\t}\n 396→\tif properties[\"services_enabled\"] != 1 {\n 397→\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n 398→\t}\n 399→\tif properties[\"services_targets\"] != 3 {"}]},"uuid":"2596e855-3412-4b41-9b66-ea85b67239ed","timestamp":"2026-03-04T10:04:22.582Z","toolUseResult":{"type":"text","file":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","content":"\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {","numLines":30,"startLine":370,"totalLines":464}},"sourceToolAssistantUUID":"874c8648-9090-4f7b-957f-21259767cee4"} +{"parentUuid":"2596e855-3412-4b41-9b66-ea85b67239ed","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01CzHdExjKXsGJFrVjUjdGMq","type":"message","role":"assistant","content":[{"type":"text","text":"Update: users 2→3, pats 4→5, idp_users_count 1→2, embedded_idp_count 2→3, add oidc assertion:"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":690,"cache_read_input_tokens":132264,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":690},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhrJ7BjV5HsVVnt2Bkup","type":"assistant","uuid":"62834aae-c861-40c4-9529-6b039bcb895b","timestamp":"2026-03-04T10:04:29.072Z"} +{"parentUuid":"62834aae-c861-40c4-9529-6b039bcb895b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01CzHdExjKXsGJFrVjUjdGMq","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01RipbdiuYdDuKxnJCks2ikJ","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","old_string":"\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}","new_string":"\tif properties[\"users\"] != 3 {\n\t\tt.Errorf(\"expected 3 users, got %d\", properties[\"users\"])\n\t}"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":690,"cache_read_input_tokens":132264,"output_tokens":253,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":690,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhrJ7BjV5HsVVnt2Bkup","type":"assistant","uuid":"df098c03-f53f-437b-ba89-943bd6737fec","timestamp":"2026-03-04T10:04:30.091Z"} +{"parentUuid":"df098c03-f53f-437b-ba89-943bd6737fec","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_01RipbdiuYdDuKxnJCks2ikJ","toolUseID":"toolu_01RipbdiuYdDuKxnJCks2ikJ","timestamp":"2026-03-04T10:04:45.322Z","uuid":"58d17255-a108-4c97-a60c-c5ba1df30428"} +{"parentUuid":"df098c03-f53f-437b-ba89-943bd6737fec","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01RipbdiuYdDuKxnJCks2ikJ","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go has been updated successfully."}]},"uuid":"568b6403-7304-4431-b61a-a726ca134c77","timestamp":"2026-03-04T10:04:45.319Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","oldString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\toidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toidcUserID: {\n\t\t\t\t\tId: oidcUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","newString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\toidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toidcUserID: {\n\t\t\t\t\tId: oidcUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 3 {\n\t\tt.Errorf(\"expected 3 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","originalFile":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\toidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toidcUserID: {\n\t\t\t\t\tId: oidcUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 2 {\n\t\tt.Errorf(\"expected 1 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n","structuredPatch":[{"oldStart":286,"oldLines":8,"newStart":286,"newLines":8,"lines":[" if properties[\"rules\"] != 4 {"," t.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])"," }","- if properties[\"users\"] != 2 {","- t.Errorf(\"expected 1 users, got %d\", properties[\"users\"])","+ if properties[\"users\"] != 3 {","+ t.Errorf(\"expected 3 users, got %d\", properties[\"users\"])"," }"," if properties[\"setup_keys_usage\"] != 2 {"," t.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"df098c03-f53f-437b-ba89-943bd6737fec"} +{"parentUuid":"568b6403-7304-4431-b61a-a726ca134c77","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01LyBCjSWdc9pFdzH8ewzYRW","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01JwahjdpSDyAuHBA3Gj7iuP","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","old_string":"\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}","new_string":"\tif properties[\"pats\"] != 5 {\n\t\tt.Errorf(\"expected 5 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":303,"cache_read_input_tokens":132954,"output_tokens":221,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":303,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhrL49EGYBT3sRVTQPoa","type":"assistant","uuid":"a9bf7852-45d5-455a-bc91-521dca53d634","timestamp":"2026-03-04T10:04:55.624Z"} +{"parentUuid":"a9bf7852-45d5-455a-bc91-521dca53d634","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_01JwahjdpSDyAuHBA3Gj7iuP","toolUseID":"toolu_01JwahjdpSDyAuHBA3Gj7iuP","timestamp":"2026-03-04T10:05:52.519Z","uuid":"9bf122f1-4805-4a6e-9091-b3561a961d21"} +{"parentUuid":"a9bf7852-45d5-455a-bc91-521dca53d634","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01JwahjdpSDyAuHBA3Gj7iuP","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go has been updated successfully."}]},"uuid":"1f549cc7-2b69-45d3-b632-231868be9221","timestamp":"2026-03-04T10:05:52.517Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","oldString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\toidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toidcUserID: {\n\t\t\t\t\tId: oidcUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 3 {\n\t\tt.Errorf(\"expected 3 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","newString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\toidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toidcUserID: {\n\t\t\t\t\tId: oidcUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 3 {\n\t\tt.Errorf(\"expected 3 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 5 {\n\t\tt.Errorf(\"expected 5 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","originalFile":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\toidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toidcUserID: {\n\t\t\t\t\tId: oidcUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 3 {\n\t\tt.Errorf(\"expected 3 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 4 {\n\t\tt.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n","structuredPatch":[{"oldStart":292,"oldLines":8,"newStart":292,"newLines":8,"lines":[" if properties[\"setup_keys_usage\"] != 2 {"," t.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])"," }","- if properties[\"pats\"] != 4 {","- t.Errorf(\"expected 4 personal_access_tokens, got %d\", properties[\"pats\"])","+ if properties[\"pats\"] != 5 {","+ t.Errorf(\"expected 5 personal_access_tokens, got %d\", properties[\"pats\"])"," }"," if properties[\"peers_ssh_enabled\"] != 2 {"," t.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"a9bf7852-45d5-455a-bc91-521dca53d634"} +{"parentUuid":"1f549cc7-2b69-45d3-b632-231868be9221","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01DK3vfmqqfFUFkiUSbJiCon","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Nk69U7rhughQE8UkJZD5UT","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","old_string":"\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}","new_string":"\tif properties[\"idp_users_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":271,"cache_read_input_tokens":133257,"output_tokens":239,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":271,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhrR1CWVUXgVi1A41rqu","type":"assistant","uuid":"82925b99-479c-4dd7-84d9-54c00e678b21","timestamp":"2026-03-04T10:06:02.568Z"} +{"parentUuid":"82925b99-479c-4dd7-84d9-54c00e678b21","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_01Nk69U7rhughQE8UkJZD5UT","toolUseID":"toolu_01Nk69U7rhughQE8UkJZD5UT","timestamp":"2026-03-04T10:06:58.662Z","uuid":"ae6fa015-430d-4ace-855e-8c94769b259e"} +{"parentUuid":"82925b99-479c-4dd7-84d9-54c00e678b21","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Nk69U7rhughQE8UkJZD5UT","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go has been updated successfully."}]},"uuid":"f65d269b-dae4-4985-8dad-251e22109bbf","timestamp":"2026-03-04T10:06:58.660Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","oldString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\toidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toidcUserID: {\n\t\t\t\t\tId: oidcUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 3 {\n\t\tt.Errorf(\"expected 3 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 5 {\n\t\tt.Errorf(\"expected 5 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","newString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\toidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toidcUserID: {\n\t\t\t\t\tId: oidcUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 3 {\n\t\tt.Errorf(\"expected 3 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 5 {\n\t\tt.Errorf(\"expected 5 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","originalFile":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\toidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toidcUserID: {\n\t\t\t\t\tId: oidcUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 3 {\n\t\tt.Errorf(\"expected 3 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 5 {\n\t\tt.Errorf(\"expected 5 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n","structuredPatch":[{"oldStart":377,"oldLines":8,"newStart":377,"newLines":8,"lines":[" if properties[\"local_users_count\"] != 1 {"," t.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])"," }","- if properties[\"idp_users_count\"] != 1 {","- t.Errorf(\"expected 1 idp_users_count, got %d\", properties[\"idp_users_count\"])","+ if properties[\"idp_users_count\"] != 2 {","+ t.Errorf(\"expected 2 idp_users_count, got %d\", properties[\"idp_users_count\"])"," }"," if properties[\"embedded_idp_users_local\"] != 1 {"," t.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"82925b99-479c-4dd7-84d9-54c00e678b21"} +{"parentUuid":"f65d269b-dae4-4985-8dad-251e22109bbf","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01PUvGAxdgDPAxj4H9smdK1x","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Qw1q23Jq51eVDNTd9RQFtj","name":"Edit","input":{"replace_all":false,"file_path":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","old_string":"\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}","new_string":"\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_users_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_oidc, got %v\", properties[\"embedded_idp_users_oidc\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 3 {\n\t\tt.Errorf(\"expected 3 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":289,"cache_read_input_tokens":133528,"output_tokens":437,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":289,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhrVXdxden6vxMgV563d","type":"assistant","uuid":"31abda54-bf04-4d52-b3d3-c6f52aabfd8c","timestamp":"2026-03-04T10:07:10.010Z"} +{"parentUuid":"31abda54-bf04-4d52-b3d3-c6f52aabfd8c","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Edit","command":"callback"},"parentToolUseID":"toolu_01Qw1q23Jq51eVDNTd9RQFtj","toolUseID":"toolu_01Qw1q23Jq51eVDNTd9RQFtj","timestamp":"2026-03-04T10:07:37.263Z","uuid":"37b4cdc7-9723-46fe-bb5e-c616eb073cdf"} +{"parentUuid":"31abda54-bf04-4d52-b3d3-c6f52aabfd8c","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Qw1q23Jq51eVDNTd9RQFtj","type":"tool_result","content":"The file /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go has been updated successfully."}]},"uuid":"0623132a-c0f2-4e8a-849b-8dd81565c886","timestamp":"2026-03-04T10:07:37.261Z","toolUseResult":{"filePath":"/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go","oldString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\toidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toidcUserID: {\n\t\t\t\t\tId: oidcUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 3 {\n\t\tt.Errorf(\"expected 3 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 5 {\n\t\tt.Errorf(\"expected 5 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","newString":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\toidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toidcUserID: {\n\t\t\t\t\tId: oidcUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 3 {\n\t\tt.Errorf(\"expected 3 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 5 {\n\t\tt.Errorf(\"expected 5 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_users_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_oidc, got %v\", properties[\"embedded_idp_users_oidc\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 3 {\n\t\tt.Errorf(\"expected 3 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}","originalFile":"package metrics\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tnbdns \"github.com/netbirdio/netbird/dns\"\n\t\"github.com/netbirdio/netbird/idp/dex\"\n\trpservice \"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service\"\n\tresourceTypes \"github.com/netbirdio/netbird/management/server/networks/resources/types\"\n\trouterTypes \"github.com/netbirdio/netbird/management/server/networks/routers/types\"\n\tnetworkTypes \"github.com/netbirdio/netbird/management/server/networks/types\"\n\tnbpeer \"github.com/netbirdio/netbird/management/server/peer\"\n\t\"github.com/netbirdio/netbird/management/server/posture\"\n\t\"github.com/netbirdio/netbird/management/server/types\"\n\t\"github.com/netbirdio/netbird/route\"\n)\n\ntype mockDatasource struct{}\n\n// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information\nfunc (mockDatasource) GetAllConnectedPeers() map[string]struct{} {\n\treturn map[string]struct{}{\n\t\t\"1\": {},\n\t}\n}\n\n// GetAllAccounts returns a list of *server.Account for use in tests with predefined information\nfunc (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {\n\tlocalUserID := dex.EncodeDexUserID(\"10\", \"local\")\n\tidpUserID := dex.EncodeDexUserID(\"20\", \"zitadel-d5uv82dra0haedlf6kv0\")\n\toidcUserID := dex.EncodeDexUserID(\"30\", \"d6jvvp69kmnc73c9pl40\")\n\treturn []*types.Account{\n\t\t{\n\t\t\tId: \"1\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: false,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSourcePostureChecks: []string{\"1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tPostureChecks: []*posture.Checks{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tName: \"tes2\",\n\t\t\t\t\tChecks: posture.ChecksDefinition{\n\t\t\t\t\t\tNBVersionCheck: &posture.NBVersionCheck{\n\t\t\t\t\t\t\tMinVersion: \"0.0.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlocalUserID: {\n\t\t\t\t\tId: localUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tServices: []*rpservice.Service{\n\t\t\t\t{\n\t\t\t\t\tID: \"svc1\",\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"peer\"},\n\t\t\t\t\t\t{TargetType: \"host\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tPasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusActive)},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"svc2\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t\tTargets: []*rpservice.Target{\n\t\t\t\t\t\t{TargetType: \"domain\"},\n\t\t\t\t\t},\n\t\t\t\t\tAuth: rpservice.AuthConfig{\n\t\t\t\t\t\tBearerAuth: &rpservice.BearerAuthConfig{Enabled: true},\n\t\t\t\t\t},\n\t\t\t\t\tMeta: rpservice.Meta{Status: string(rpservice.StatusPending)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tId: \"2\",\n\t\t\tSettings: &types.Settings{PeerLoginExpirationEnabled: true},\n\t\t\tSetupKeys: map[string]*types.SetupKey{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tEphemeral: true,\n\t\t\t\t\tUsedTimes: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: map[string]*types.Group{\n\t\t\t\t\"1\": {},\n\t\t\t\t\"2\": {},\n\t\t\t},\n\t\t\tNameServerGroups: map[string]*nbdns.NameServerGroup{\n\t\t\t\t\"1\": {},\n\t\t\t},\n\t\t\tPeers: map[string]*nbpeer.Peer{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tUserID: \"test\",\n\t\t\t\t\tSSHEnabled: true,\n\t\t\t\t\tMeta: nbpeer.PeerSystemMeta{GoOS: \"linux\", WtVersion: \"0.0.1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPolicies: []*types.Policy{\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: true,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRules: []*types.PolicyRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBidirectional: false,\n\t\t\t\t\t\t\tProtocol: types.PolicyRuleProtocolTCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoutes: map[route.ID]*route.Route{\n\t\t\t\t\"1\": {\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tPeerGroups: make([]string, 1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tUsers: map[string]*types.User{\n\t\t\t\t\"1\": {\n\t\t\t\t\tId: \"1\",\n\t\t\t\t\tIsServiceUser: true,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tidpUserID: {\n\t\t\t\t\tId: idpUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toidcUserID: {\n\t\t\t\t\tId: oidcUserID,\n\t\t\t\t\tIsServiceUser: false,\n\t\t\t\t\tPATs: map[string]*types.PersonalAccessToken{\n\t\t\t\t\t\t\"1\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworks: []*networkTypes.Network{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkResources: []*resourceTypes.NetworkResource{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID: \"2\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tNetworkRouters: []*routerTypes.NetworkRouter{\n\t\t\t\t{\n\t\t\t\t\tID: \"1\",\n\t\t\t\t\tAccountID: \"1\",\n\t\t\t\t\tNetworkID: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// GetStoreEngine returns FileStoreEngine\nfunc (mockDatasource) GetStoreEngine() types.Engine {\n\treturn types.FileStoreEngine\n}\n\n// GetCustomDomainsCounts returns test custom domain counts.\nfunc (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {\n\treturn 3, 2, nil\n}\n\n// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties\nfunc TestGenerateProperties(t *testing.T) {\n\tds := mockDatasource{}\n\tworker := Worker{\n\t\tdataSource: ds,\n\t\tconnManager: ds,\n\t\tidpManager: EmbeddedType,\n\t}\n\n\tproperties := worker.generateProperties(context.Background())\n\n\tif properties[\"accounts\"] != 2 {\n\t\tt.Errorf(\"expected 2 accounts, got %d\", properties[\"accounts\"])\n\t}\n\tif properties[\"peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers, got %d\", properties[\"peers\"])\n\t}\n\tif properties[\"routes\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes, got %d\", properties[\"routes\"])\n\t}\n\tif properties[\"networks\"] != 1 {\n\t\tt.Errorf(\"expected 1 networks, got %d\", properties[\"networks\"])\n\t}\n\tif properties[\"network_resources\"] != 2 {\n\t\tt.Errorf(\"expected 2 network_resources, got %d\", properties[\"network_resources\"])\n\t}\n\tif properties[\"network_routers\"] != 1 {\n\t\tt.Errorf(\"expected 1 network_routers, got %d\", properties[\"network_routers\"])\n\t}\n\tif properties[\"rules\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules, got %d\", properties[\"rules\"])\n\t}\n\tif properties[\"users\"] != 3 {\n\t\tt.Errorf(\"expected 3 users, got %d\", properties[\"users\"])\n\t}\n\tif properties[\"setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 1 setup_keys_usage, got %d\", properties[\"setup_keys_usage\"])\n\t}\n\tif properties[\"pats\"] != 5 {\n\t\tt.Errorf(\"expected 5 personal_access_tokens, got %d\", properties[\"pats\"])\n\t}\n\tif properties[\"peers_ssh_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_ssh_enabled, got %d\", properties[\"peers_ssh_enabled\"])\n\t}\n\tif properties[\"routes_with_routing_groups\"] != 2 {\n\t\tt.Errorf(\"expected 2 routes_with_routing_groups, got %d\", properties[\"routes_with_routing_groups\"])\n\t}\n\tif properties[\"rules_protocol_tcp\"] != 4 {\n\t\tt.Errorf(\"expected 4 rules_protocol_tcp, got %d\", properties[\"rules_protocol_tcp\"])\n\t}\n\tif properties[\"rules_direction_oneway\"] != 2 {\n\t\tt.Errorf(\"expected 2 rules_direction_oneway, got %d\", properties[\"rules_direction_oneway\"])\n\t}\n\n\tif properties[\"active_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_peers_last_day, got %d\", properties[\"active_peers_last_day\"])\n\t}\n\tif properties[\"min_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 min_active_peer_version, got %s\", properties[\"min_active_peer_version\"])\n\t}\n\tif properties[\"max_active_peer_version\"] != \"0.0.1\" {\n\t\tt.Errorf(\"expected 0.0.1 max_active_peer_version, got %s\", properties[\"max_active_peer_version\"])\n\t}\n\n\tif properties[\"peers_login_expiration_enabled\"] != 2 {\n\t\tt.Errorf(\"expected 2 peers_login_expiration_enabled, got %d\", properties[\"peers_login_expiration_enabled\"])\n\t}\n\n\tif properties[\"service_users\"] != 2 {\n\t\tt.Errorf(\"expected 2 service_users, got %d\", properties[\"service_users\"])\n\t}\n\n\tif properties[\"peer_os_linux\"] != 2 {\n\t\tt.Errorf(\"expected 2 peer_os_linux, got %d\", properties[\"peer_os_linux\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"ephemeral_peers_setup_keys_usage\"] != 2 {\n\t\tt.Errorf(\"expected 2 ephemeral_peers_setup_keys_usage, got %d\", properties[\"ephemeral_peers_setup_keys_usage\"])\n\t}\n\n\tif properties[\"nameservers\"] != 2 {\n\t\tt.Errorf(\"expected 2 nameservers, got %d\", properties[\"nameservers\"])\n\t}\n\n\tif properties[\"groups\"] != 4 {\n\t\tt.Errorf(\"expected 4 groups, got %d\", properties[\"groups\"])\n\t}\n\n\tif properties[\"user_peers\"] != 2 {\n\t\tt.Errorf(\"expected 2 user_peers, got %d\", properties[\"user_peers\"])\n\t}\n\n\tif properties[\"store_engine\"] != types.FileStoreEngine {\n\t\tt.Errorf(\"expected JsonFile, got %s\", properties[\"store_engine\"])\n\t}\n\n\tif properties[\"rules_with_src_posture_checks\"] != 1 {\n\t\tt.Errorf(\"expected 1 rules_with_src_posture_checks, got %d\", properties[\"rules_with_src_posture_checks\"])\n\t}\n\n\tif properties[\"posture_checks\"] != 2 {\n\t\tt.Errorf(\"expected 2 posture_checks, got %d\", properties[\"posture_checks\"])\n\t}\n\n\tif properties[\"rosenpass_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 rosenpass_enabled, got %d\", properties[\"rosenpass_enabled\"])\n\t}\n\n\tif properties[\"active_user_peers_last_day\"] != 2 {\n\t\tt.Errorf(\"expected 2 active_user_peers_last_day, got %d\", properties[\"active_user_peers_last_day\"])\n\t}\n\n\tif properties[\"active_users_last_day\"] != 1 {\n\t\tt.Errorf(\"expected 1 active_users_last_day, got %d\", properties[\"active_users_last_day\"])\n\t}\n\n\tif properties[\"local_users_count\"] != 1 {\n\t\tt.Errorf(\"expected 1 local_users_count, got %d\", properties[\"local_users_count\"])\n\t}\n\tif properties[\"idp_users_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 idp_users_count, got %d\", properties[\"idp_users_count\"])\n\t}\n\tif properties[\"embedded_idp_users_local\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_local, got %v\", properties[\"embedded_idp_users_local\"])\n\t}\n\tif properties[\"embedded_idp_users_zitadel\"] != 1 {\n\t\tt.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])\n\t}\n\tif properties[\"embedded_idp_count\"] != 2 {\n\t\tt.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])\n\t}\n\n\tif properties[\"services\"] != 2 {\n\t\tt.Errorf(\"expected 2 services, got %v\", properties[\"services\"])\n\t}\n\tif properties[\"services_enabled\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_enabled, got %v\", properties[\"services_enabled\"])\n\t}\n\tif properties[\"services_targets\"] != 3 {\n\t\tt.Errorf(\"expected 3 services_targets, got %v\", properties[\"services_targets\"])\n\t}\n\tif properties[\"services_status_active\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_active, got %v\", properties[\"services_status_active\"])\n\t}\n\tif properties[\"services_status_pending\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_status_pending, got %v\", properties[\"services_status_pending\"])\n\t}\n\tif properties[\"services_status_error\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_status_error, got %v\", properties[\"services_status_error\"])\n\t}\n\tif properties[\"services_target_type_peer\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_peer, got %v\", properties[\"services_target_type_peer\"])\n\t}\n\tif properties[\"services_target_type_host\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_host, got %v\", properties[\"services_target_type_host\"])\n\t}\n\tif properties[\"services_target_type_domain\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_target_type_domain, got %v\", properties[\"services_target_type_domain\"])\n\t}\n\tif properties[\"services_auth_password\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_password, got %v\", properties[\"services_auth_password\"])\n\t}\n\tif properties[\"services_auth_oidc\"] != 1 {\n\t\tt.Errorf(\"expected 1 services_auth_oidc, got %v\", properties[\"services_auth_oidc\"])\n\t}\n\tif properties[\"services_auth_pin\"] != 0 {\n\t\tt.Errorf(\"expected 0 services_auth_pin, got %v\", properties[\"services_auth_pin\"])\n\t}\n\tif properties[\"custom_domains\"] != int64(3) {\n\t\tt.Errorf(\"expected 3 custom_domains, got %v\", properties[\"custom_domains\"])\n\t}\n\tif properties[\"custom_domains_validated\"] != int64(2) {\n\t\tt.Errorf(\"expected 2 custom_domains_validated, got %v\", properties[\"custom_domains_validated\"])\n\t}\n}\n\nfunc TestExtractIdpType(t *testing.T) {\n\ttests := []struct {\n\t\tconnectorID string\n\t\texpected string\n\t}{\n\t\t{\"okta-abc123def\", \"okta\"},\n\t\t{\"zitadel-d5uv82dra0haedlf6kv0\", \"zitadel\"},\n\t\t{\"entra-xyz789\", \"entra\"},\n\t\t{\"google-abc123\", \"google\"},\n\t\t{\"pocketid-abc123\", \"pocketid\"},\n\t\t{\"microsoft-abc123\", \"microsoft\"},\n\t\t{\"authentik-abc123\", \"authentik\"},\n\t\t{\"keycloak-d5uv82dra0haedlf6kv0\", \"keycloak\"},\n\t\t{\"local\", \"local\"},\n\t\t{\"d6jvvp69kmnc73c9pl40\", \"oidc\"},\n\t\t{\"\", \"oidc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.connectorID, func(t *testing.T) {\n\t\t\tresult := extractIdpType(tt.connectorID)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"extractIdpType(%q) = %q, want %q\", tt.connectorID, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n","structuredPatch":[{"oldStart":386,"oldLines":9,"newStart":386,"newLines":12,"lines":[" if properties[\"embedded_idp_users_zitadel\"] != 1 {"," t.Errorf(\"expected 1 embedded_idp_users_zitadel, got %v\", properties[\"embedded_idp_users_zitadel\"])"," }","- if properties[\"embedded_idp_count\"] != 2 {","- t.Errorf(\"expected 2 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])","+ if properties[\"embedded_idp_users_oidc\"] != 1 {","+ t.Errorf(\"expected 1 embedded_idp_users_oidc, got %v\", properties[\"embedded_idp_users_oidc\"])"," }","+ if properties[\"embedded_idp_count\"] != 3 {","+ t.Errorf(\"expected 3 embedded_idp_count, got %v\", properties[\"embedded_idp_count\"])","+ }"," "," if properties[\"services\"] != 2 {"," t.Errorf(\"expected 2 services, got %v\", properties[\"services\"])"]}],"userModified":false,"replaceAll":false},"sourceToolAssistantUUID":"31abda54-bf04-4d52-b3d3-c6f52aabfd8c"} +{"parentUuid":"0623132a-c0f2-4e8a-849b-8dd81565c886","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01CHMgnNUEa2K7biVdJritJv","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01CxCvB1uxEm5hbBQZsG2m9f","name":"Bash","input":{"command":"go test ./management/server/metrics/... -run 'TestExtractIdpType|TestGenerateProperties' -v 2>&1","description":"Run metrics tests","timeout":120000},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":487,"cache_read_input_tokens":133817,"output_tokens":120,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":487,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"}},"requestId":"req_011CYhrYPLy9M8DCrs9vVKs8","type":"assistant","uuid":"e90864fe-30d7-4975-a36b-fbba42651096","timestamp":"2026-03-04T10:07:42.394Z"} +{"parentUuid":"e90864fe-30d7-4975-a36b-fbba42651096","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":3,"totalLines":0,"totalBytes":0,"taskId":"bk69pstvn","timeoutMs":120000},"toolUseID":"bash-progress-0","parentToolUseID":"toolu_01CxCvB1uxEm5hbBQZsG2m9f","uuid":"ecce6715-b71b-4d2d-b56c-1f8fcd29ac11","timestamp":"2026-03-04T10:07:45.491Z"} +{"parentUuid":"ecce6715-b71b-4d2d-b56c-1f8fcd29ac11","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"bash_progress","output":"","fullOutput":"","elapsedTimeSeconds":4,"totalLines":0,"totalBytes":0,"taskId":"bk69pstvn","timeoutMs":120000},"toolUseID":"bash-progress-1","parentToolUseID":"toolu_01CxCvB1uxEm5hbBQZsG2m9f","uuid":"7daf288c-f9cf-42fe-ac7d-140074b5f709","timestamp":"2026-03-04T10:07:46.493Z"} +{"parentUuid":"e90864fe-30d7-4975-a36b-fbba42651096","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01CxCvB1uxEm5hbBQZsG2m9f","type":"tool_result","content":"=== RUN TestGenerateProperties\n--- PASS: TestGenerateProperties (0.00s)\n=== RUN TestExtractIdpType\n=== RUN TestExtractIdpType/okta-abc123def\n=== RUN REDACTED\n=== RUN TestExtractIdpType/entra-xyz789\n=== RUN TestExtractIdpType/google-abc123\n=== RUN TestExtractIdpType/pocketid-abc123\n=== RUN TestExtractIdpType/microsoft-abc123\n=== RUN TestExtractIdpType/authentik-abc123\n=== RUN REDACTED\n=== RUN TestExtractIdpType/local\n=== RUN REDACTED\n=== RUN TestExtractIdpType/#00\n--- PASS: TestExtractIdpType (0.00s)\n --- PASS: TestExtractIdpType/okta-abc123def (0.00s)\n --- PASS: REDACTED (0.00s)\n --- PASS: TestExtractIdpType/entra-xyz789 (0.00s)\n --- PASS: TestExtractIdpType/google-abc123 (0.00s)\n --- PASS: TestExtractIdpType/pocketid-abc123 (0.00s)\n --- PASS: TestExtractIdpType/microsoft-abc123 (0.00s)\n --- PASS: TestExtractIdpType/authentik-abc123 (0.00s)\n --- PASS: REDACTED (0.00s)\n --- PASS: TestExtractIdpType/local (0.00s)\n --- PASS: REDACTED (0.00s)\n --- PASS: TestExtractIdpType/#00 (0.00s)\nPASS\nok \tgithub.com/netbirdio/netbird/management/server/metrics\t0.900s","is_error":false}]},"uuid":"34afcd16-c26a-405c-9ce1-e184ce1c408c","timestamp":"2026-03-04T10:07:46.761Z","toolUseResult":{"stdout":"=== RUN TestGenerateProperties\n--- PASS: TestGenerateProperties (0.00s)\n=== RUN TestExtractIdpType\n=== RUN TestExtractIdpType/okta-abc123def\n=== RUN REDACTED\n=== RUN TestExtractIdpType/entra-xyz789\n=== RUN TestExtractIdpType/google-abc123\n=== RUN TestExtractIdpType/pocketid-abc123\n=== RUN TestExtractIdpType/microsoft-abc123\n=== RUN TestExtractIdpType/authentik-abc123\n=== RUN REDACTED\n=== RUN TestExtractIdpType/local\n=== RUN REDACTED\n=== RUN TestExtractIdpType/#00\n--- PASS: TestExtractIdpType (0.00s)\n --- PASS: TestExtractIdpType/okta-abc123def (0.00s)\n --- PASS: REDACTED (0.00s)\n --- PASS: TestExtractIdpType/entra-xyz789 (0.00s)\n --- PASS: TestExtractIdpType/google-abc123 (0.00s)\n --- PASS: TestExtractIdpType/pocketid-abc123 (0.00s)\n --- PASS: TestExtractIdpType/microsoft-abc123 (0.00s)\n --- PASS: TestExtractIdpType/authentik-abc123 (0.00s)\n --- PASS: REDACTED (0.00s)\n --- PASS: TestExtractIdpType/local (0.00s)\n --- PASS: REDACTED (0.00s)\n --- PASS: TestExtractIdpType/#00 (0.00s)\nPASS\nok \tgithub.com/netbirdio/netbird/management/server/metrics\t0.900s","stderr":"","interrupted":false,"isImage":false,"noOutputExpected":false},"sourceToolAssistantUUID":"e90864fe-30d7-4975-a36b-fbba42651096"} +{"parentUuid":"34afcd16-c26a-405c-9ce1-e184ce1c408c","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_01TLv7x4TfgPrtXYbc2jGhMA","type":"message","role":"assistant","content":[{"type":"text","text":"All pass. The `TestGenerateProperties` now covers all three connector types with real dex-encoded user IDs:\n\n- `local` → `embedded_idp_users_local = 1`\n- `zitadel-d5uv82dra0haedlf6kv0` (prefixed) → `embedded_idp_users_zitadel = 1`\n- `d6jvvp69kmnc73c9pl40` (generic OIDC, no prefix) → `embedded_idp_users_oidc = 1`\n- `embedded_idp_count = 3` (local + zitadel + oidc)"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":750,"cache_read_input_tokens":134304,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":750},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhrZ6hdWCZtANcmtudos","type":"assistant","uuid":"59bf5d18-cbcc-4c39-9277-78e099b88271","timestamp":"2026-03-04T10:07:53.544Z"} +{"parentUuid":"59bf5d18-cbcc-4c39-9277-78e099b88271","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"Stop","hookName":"Stop","command":"entire hooks claude-code stop"},"parentToolUseID":"2d287a1f-3eaf-4c09-9412-3c98439770c9","toolUseID":"2d287a1f-3eaf-4c09-9412-3c98439770c9","timestamp":"2026-03-04T10:07:53.717Z","uuid":"f0ee17f1-20b2-44a9-aac5-569788577cad"} +{"parentUuid":"f0ee17f1-20b2-44a9-aac5-569788577cad","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"system","subtype":"stop_hook_summary","hookCount":1,"hookInfos":[{"command":"entire hooks claude-code stop","durationMs":1126}],"hookErrors":[],"preventedContinuation":false,"stopReason":"","hasOutput":true,"level":"suggestion","timestamp":"2026-03-04T10:07:54.845Z","uuid":"d13e7adc-8151-4f56-8174-6e00eceb60cd","toolUseID":"2d287a1f-3eaf-4c09-9412-3c98439770c9"} +{"parentUuid":"d13e7adc-8151-4f56-8174-6e00eceb60cd","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"main","slug":"jaunty-mapping-trinket","type":"system","subtype":"turn_duration","durationMs":158654,"timestamp":"2026-03-04T10:07:54.847Z","uuid":"58e34723-5236-4498-9424-249a0617cea4","isMeta":false} +{"type":"file-history-snapshot","messageId":"39823226-baa2-4c84-8ded-d86374ae2124","snapshot":{"messageId":"39823226-baa2-4c84-8ded-d86374ae2124","trackedFileBackups":{"combined/cmd/config.go":{"backupFileName":"9fe7f46846e64b1a@v3","version":3,"backupTime":"2026-03-03T09:45:34.548Z"},"management/server/store/sql_store.go":{"backupFileName":"4799e38f203744f8@v3","version":3,"backupTime":"2026-03-04T09:35:30.335Z"},"management/server/activity/store/sql_store.go":{"backupFileName":"7fa4affe872004f2@v2","version":2,"backupTime":"2026-03-02T19:45:28.129Z"},"combined/cmd/root.go":{"backupFileName":"b13aacc820021d9f@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/cmd/token.go":{"backupFileName":"5796305e24f183be@v2","version":2,"backupTime":"2026-03-02T19:45:28.130Z"},"combined/config.yaml.example":{"backupFileName":"ab8acefcb43dda84@v2","version":2,"backupTime":"2026-03-02T19:45:28.132Z"},"management/server/metrics/selfhosted.go":{"backupFileName":"e4e6cf8f05310807@v3","version":3,"backupTime":"2026-03-04T09:59:31.749Z"},"management/server/metrics/selfhosted_test.go":{"backupFileName":"a0ee876802b49262@v4","version":4,"backupTime":"2026-03-04T10:09:25.225Z"}},"timestamp":"2026-03-04T10:09:25.221Z"},"isSnapshotUpdate":false} +{"parentUuid":"58e34723-5236-4498-9424-249a0617cea4","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"fix/selfhosted-idp-metrics-report","slug":"jaunty-mapping-trinket","type":"user","message":{"role":"user","content":"create a pr description and a title"},"uuid":"39823226-baa2-4c84-8ded-d86374ae2124","timestamp":"2026-03-04T10:09:23.484Z","permissionMode":"default"} +{"parentUuid":"39823226-baa2-4c84-8ded-d86374ae2124","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"fix/selfhosted-idp-metrics-report","slug":"jaunty-mapping-trinket","message":{"model":"claude-opus-4-6","id":"msg_016sYPgE9pbzrBcScpHrtSBT","type":"message","role":"assistant","content":[{"type":"text","text":"\n\n**Title:** Fix embedded IdP metrics to count local and generic OIDC users\n\n**Description:**\n\n## Summary\n- Fix `extractIdpType` to return `\"local\"` for local connector IDs instead of `\"oidc\"`, and `\"oidc\"` for generic OIDC connectors (bare xid, no type prefix)\n- Include local users in `embeddedIdpTypes` map so `embedded_idp_count` and `embedded_idp_users_local` metrics are emitted\n- Add generic OIDC user (no-prefix connector) to test data to cover all three connector types\n\n## What was broken\n- `embedded_idp_count` excluded local as a type, understating the number of configured IdP types\n- No `embedded_idp_users_local` metric was emitted despite local users being tracked separately\n- `extractIdpType(\"local\")` would have returned `\"oidc\"` (wrong) if ever called directly\n\n## Test plan\n- [x] `TestExtractIdpType` covers local, generic OIDC (bare xid), prefixed connectors, and empty string\n- [x] `TestGenerateProperties` validates all three user types with real dex-encoded IDs: local, zitadel (prefixed), and generic OIDC (no prefix)"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":184,"cache_read_input_tokens":135216,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":184},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}},"requestId":"req_011CYhrgKCQMYUMPmueTdhks","type":"assistant","uuid":"877ababe-2e0d-40aa-aa5c-a2d55eb25958","timestamp":"2026-03-04T10:09:36.311Z"} +{"parentUuid":"877ababe-2e0d-40aa-aa5c-a2d55eb25958","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"fix/selfhosted-idp-metrics-report","slug":"jaunty-mapping-trinket","type":"progress","data":{"type":"hook_progress","hookEvent":"Stop","hookName":"Stop","command":"entire hooks claude-code stop"},"parentToolUseID":"c80b2ef3-b7e6-49a0-9094-c950cfe94867","toolUseID":"c80b2ef3-b7e6-49a0-9094-c950cfe94867","timestamp":"2026-03-04T10:09:36.486Z","uuid":"c40c2f72-741c-4b6e-88d0-960ec7e3550b"} +{"parentUuid":"c40c2f72-741c-4b6e-88d0-960ec7e3550b","isSidechain":false,"userType":"external","cwd":"/Users/misha/Documents/GolandProjects/netbird/netbird","sessionId":"234e6a5f-73bd-4819-9dcf-9254a5fe0da3","version":"2.1.63","gitBranch":"fix/selfhosted-idp-metrics-report","slug":"jaunty-mapping-trinket","type":"system","subtype":"stop_hook_summary","hookCount":1,"hookInfos":[{"command":"entire hooks claude-code stop","durationMs":690}],"hookErrors":[],"preventedContinuation":false,"stopReason":"","hasOutput":true,"level":"suggestion","timestamp":"2026-03-04T10:09:37.178Z","uuid":"a34db900-014e-4512-95cc-a494730c270a","toolUseID":"c80b2ef3-b7e6-49a0-9094-c950cfe94867"} diff --git a/39/f0b252e2a9/0/metadata.json b/39/f0b252e2a9/0/metadata.json new file mode 100644 index 000000000..60b7f4618 --- /dev/null +++ b/39/f0b252e2a9/0/metadata.json @@ -0,0 +1,33 @@ +{ + "cli_version": "0.4.2", + "checkpoint_id": "39f0b252e2a9", + "session_id": "234e6a5f-73bd-4819-9dcf-9254a5fe0da3", + "strategy": "manual-commit", + "created_at": "2026-03-04T10:09:54.85448Z", + "branch": "fix/selfhosted-idp-metrics-report", + "checkpoints_count": 3, + "files_touched": [ + "management/server/metrics/selfhosted.go", + "management/server/metrics/selfhosted_test.go" + ], + "agent": "Claude Code", + "transcript_identifier_at_start": "b8bf8f95-b9b5-4e5e-893e-d40a43a7c7c8", + "checkpoint_transcript_start": 289, + "transcript_lines_at_start": 289, + "token_usage": { + "input_tokens": 72, + "cache_creation_tokens": 120843, + "cache_read_tokens": 5950426, + "output_tokens": 19221, + "api_call_count": 52 + }, + "initial_attribution": { + "calculated_at": "2026-03-04T10:09:54.571727Z", + "agent_lines": 44, + "human_added": 99868, + "human_modified": 0, + "human_removed": 0, + "total_committed": 99912, + "agent_percentage": 0.04403875410361118 + } +} diff --git a/39/f0b252e2a9/0/prompt.txt b/39/f0b252e2a9/0/prompt.txt new file mode 100644 index 000000000..6f5992e61 --- /dev/null +++ b/39/f0b252e2a9/0/prompt.txt @@ -0,0 +1,130 @@ +how datadir is used in the combined/ + +--- + +add support of providing a file of the sqlite storage if sqlite engine is specified in combined/ for store, authStore and activityStore + +--- + +[Request interrupted by user for tool use] + +--- + +we need to chevck if teh user provided query params to the file after ? + +--- + +[Request interrupted by user for tool use] + +--- + +question, do we need to do the query separation for activity store? + +--- + +summarize the changes in a short pr description + +--- + +Verify each finding against the current code and only fix it if needed. + +In `@combined/cmd/config.go` around lines 572 - 574, The assignment of +c.Server.AuthStore.File to authStorageFile should resolve relative paths against +mgmt.DataDir so the auth DB lives under the management data directory like the +other sqlite stores; update the logic where authStorageFile is set (referencing +c.Server.AuthStore.File and authStorageFile) to check if the value is non-empty +and not an absolute path (use filepath.IsAbs) and if so join it with +mgmt.DataDir (use filepath.Join and optionally filepath.Clean) before assigning; +keep absolute paths unchanged. + +--- + +Relative paths like "custom_idp.db" are now resolved against mgmt.DataDir + +--- + +decode this dex user id: REDACTED + +--- + +this one REDACTED and this one e1badda4-2a65-458c-aca0-b32c8e2b8a77 + +--- + +[Request interrupted by user] + +--- + +this one REDACTED + +--- + +decode these users, the first column is id: REDACTED|d67qqb69kmnc73b2nbm0|owner|0|0||[]|0|0|2026-02-22 14:49:17.460665591+00:00|2026-02-13 23:01:00.235815714+00:00|api|0||REDACTED|REDACTED +REDACTED|d67qqb69kmnc73b2nbm0|admin|0|0||[]|0|0||2026-02-13 23:07:34.026260389+00:00|api|0||REDACTED|REDACTED +REDACTED|d67qqb69kmnc73b2nbm0|admin|0|0||["d67qth69kmnc73b2nli0"]|0|0||2026-02-13 23:12:13.963185087+00:00|api|0||REDACTED|REDACTED +REDACTED|d67qqb69kmnc73b2nbm0|user|0|0||["d67qth69kmnc73b2nli0"]|0|0||2026-02-13 23:39:32.97851149+00:00|api|0||REDACTED|REDACTED +REDACTED|d67qqb69kmnc73b2nbm0|admin|0|0||[]|0|0|2026-02-19 09:44:19.187236451+00:00|2026-02-16 18:22:33.166228524+00:00|api|0||REDACTED|REDACTED + +--- + +add the original stored id + +--- + +add a column after decoding base64 + +--- + +in the // Returns the type prefix, or "oidc" if no known prefix is found. +func extractIdpType(connectorID string) string { + idx := strings.LastIndex(connectorID, "-") + if idx <= 0 { + return "oidc" + } + return strings.ToLower(connectorID[:idx]) +} in management/server/metrics/selfhosted.go I think that we don't count local or maybe counting it wrong. Could you please check it, fix it and add a test? + +--- + +liek before, decode these: REDACTED +REDACTED +REDACTED +REDACTED +REDACTED +REDACTED + +--- + +if there is no prefix - this is generic oidc: func generateIdentityProviderID(idpType types.IdentityProviderType) string { + id := xid.New().String() + + switch idpType { + case types.IdentityProviderTypeOkta: + return "okta-" + id + case types.IdentityProviderTypeZitadel: + return "zitadel-" + id + case types.IdentityProviderTypeEntra: + return "entra-" + id + case types.IdentityProviderTypeGoogle: + return "google-" + id + case types.IdentityProviderTypePocketID: + return "pocketid-" + id + case types.IdentityProviderTypeMicrosoft: + return "microsoft-" + id + case types.IdentityProviderTypeAuthentik: + return "authentik-" + id + case types.IdentityProviderTypeKeycloak: + return "keycloak-" + id + default: + // Generic OIDC - no prefix + return id + } +} + +--- + +you need to test generateProperties() with real dex-encoded userids too + +--- + +create a pr description and a title \ No newline at end of file diff --git a/39/f0b252e2a9/metadata.json b/39/f0b252e2a9/metadata.json new file mode 100644 index 000000000..c3e2f58a7 --- /dev/null +++ b/39/f0b252e2a9/metadata.json @@ -0,0 +1,27 @@ +{ + "cli_version": "0.4.2", + "checkpoint_id": "39f0b252e2a9", + "strategy": "manual-commit", + "branch": "fix/selfhosted-idp-metrics-report", + "checkpoints_count": 3, + "files_touched": [ + "management/server/metrics/selfhosted.go", + "management/server/metrics/selfhosted_test.go" + ], + "sessions": [ + { + "metadata": "/39/f0b252e2a9/0/metadata.json", + "transcript": "/39/f0b252e2a9/0/full.jsonl", + "context": "/39/f0b252e2a9/0/context.md", + "content_hash": "/39/f0b252e2a9/0/content_hash.txt", + "prompt": "/39/f0b252e2a9/0/prompt.txt" + } + ], + "token_usage": { + "input_tokens": 72, + "cache_creation_tokens": 120843, + "cache_read_tokens": 5950426, + "output_tokens": 19221, + "api_call_count": 52 + } +}