//! Configuration for the Libertaria Capsule Node. const std = @import("std"); pub const NodeConfig = struct { /// Data directory for persistent state (DB, keys, etc.) data_dir: []const u8, /// UTCP bind port (default: 8710) port: u16 = 8710, /// Control Socket Path (Unix Domain Socket) control_socket_path: []const u8 = "", /// Identity Key Path (Ed25519 private key) identity_key_path: []const u8 = "", /// Bootstrap peers (multiaddrs) bootstrap_peers: [][]const u8 = &.{}, /// Logging level log_level: std.log.Level = .info, /// Enable Gateway Service (Layer 1 Coordination) gateway_enabled: bool = false, /// Enable Relay Service (Layer 2 Forwarding) relay_enabled: bool = false, /// Enable Bridge Service (Layer 3 Protocol Translation) bridge_enabled: bool = false, /// QVL minimum trust score for relay selection relay_trust_threshold: f64 = 0.5, /// Free allocated memory (strings, slices) pub fn deinit(self: *NodeConfig, allocator: std.mem.Allocator) void { allocator.free(self.data_dir); allocator.free(self.control_socket_path); allocator.free(self.identity_key_path); for (self.bootstrap_peers) |peer| { allocator.free(peer); } allocator.free(self.bootstrap_peers); } pub fn default(allocator: std.mem.Allocator) !NodeConfig { // Default data dir: ~/.libertaria (or "data" for MVP) return NodeConfig{ .data_dir = try allocator.dupe(u8, "data"), .control_socket_path = try allocator.dupe(u8, "data/capsule.sock"), .identity_key_path = try allocator.dupe(u8, "data/identity.key"), .port = 8710, .gateway_enabled = false, .relay_enabled = false, .bridge_enabled = false, .relay_trust_threshold = 0.5, }; } /// Load configuration from a JSON file pub fn loadFromJsonFile(allocator: std.mem.Allocator, path: []const u8) !NodeConfig { const file = std.fs.cwd().openFile(path, .{}) catch |err| { if (err == error.FileNotFound) { // If config missing, create default std.log.info("Config file not found at {s}, creating default...", .{path}); const cfg = try NodeConfig.default(allocator); try cfg.saveToJsonFile(path); return cfg; } return err; }; defer file.close(); const max_size = 1024 * 1024; // 1MB config limit const content = try file.readToEndAlloc(allocator, max_size); defer allocator.free(content); // Parse JSON const parsed = try std.json.parseFromSlice(NodeConfig, allocator, content, .{ .allocate = .alloc_always, }); defer parsed.deinit(); // Deep copy strings because parsed.value shares memory with arena/content in some modes, // but here we used alloc_always so fields are allocated. // However, std.json.parseFromSlice returns a Parsed(T) which manages the memory. // We need to detach or copy the data to return a standalone NodeConfig. // For simplicity and safety: manually duplicate into new struct. const cfg = parsed.value; const data_dir = try allocator.dupe(u8, cfg.data_dir); const control_socket_path = if (cfg.control_socket_path.len > 0) try allocator.dupe(u8, cfg.control_socket_path) else try std.fmt.allocPrint(allocator, "{s}/capsule.sock", .{data_dir}); const identity_key_path = if (cfg.identity_key_path.len > 0) try allocator.dupe(u8, cfg.identity_key_path) else try std.fmt.allocPrint(allocator, "{s}/identity.key", .{data_dir}); var peers = std.array_list.Managed([]const u8).init(allocator); for (cfg.bootstrap_peers) |peer| { try peers.append(try allocator.dupe(u8, peer)); } return NodeConfig{ .data_dir = data_dir, .control_socket_path = control_socket_path, .identity_key_path = identity_key_path, .port = cfg.port, .bootstrap_peers = try peers.toOwnedSlice(), .log_level = cfg.log_level, .gateway_enabled = cfg.gateway_enabled, }; } /// Save configuration to a JSON file pub fn saveToJsonFile(self: *const NodeConfig, path: []const u8) !void { const file = try std.fs.cwd().createFile(path, .{}); defer file.close(); var buf = std.array_list.Managed(u8).init(std.heap.page_allocator); defer buf.deinit(); try buf.writer().print("{f}", .{std.json.fmt(self, .{ .whitespace = .indent_4 })}); try file.writeAll(buf.items); } };