Files
didactyl/src/config.c

760 lines
24 KiB
C

#define _POSIX_C_SOURCE 200809L
#include "config.h"
#include <ctype.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cjson/cJSON.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
static char g_config_last_error[512] = {0};
static void config_set_error(const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
vsnprintf(g_config_last_error, sizeof(g_config_last_error), fmt ? fmt : "unknown configuration error", ap);
va_end(ap);
}
const char* config_last_error(void) {
return g_config_last_error[0] != '\0' ? g_config_last_error : "unknown configuration error";
}
static int read_file_to_buffer(const char* path, char** out_buf, size_t* out_len) {
FILE* fp = fopen(path, "rb");
if (!fp) {
return -1;
}
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return -1;
}
long len = ftell(fp);
if (len < 0) {
fclose(fp);
return -1;
}
if (fseek(fp, 0, SEEK_SET) != 0) {
fclose(fp);
return -1;
}
char* buf = (char*)malloc((size_t)len + 1U);
if (!buf) {
fclose(fp);
return -1;
}
size_t read_len = fread(buf, 1, (size_t)len, fp);
fclose(fp);
if (read_len != (size_t)len) {
free(buf);
return -1;
}
buf[len] = '\0';
*out_buf = buf;
*out_len = (size_t)len;
return 0;
}
static int copy_json_string(cJSON* obj, const char* key, char* dst, size_t dst_size, int required) {
cJSON* item = cJSON_GetObjectItemCaseSensitive(obj, key);
if (!item || !cJSON_IsString(item) || !item->valuestring) {
return required ? -1 : 0;
}
size_t n = strlen(item->valuestring);
if (n >= dst_size) {
return -1;
}
memcpy(dst, item->valuestring, n + 1U);
return 0;
}
static int is_hex64(const char* s) {
if (!s || strlen(s) != 64U) {
return 0;
}
for (size_t i = 0; i < 64U; i++) {
if (!isxdigit((unsigned char)s[i])) {
return 0;
}
}
return 1;
}
static int decode_private_key(const char* nsec_or_hex, unsigned char out_private_key[32]) {
if (!nsec_or_hex || !out_private_key) {
return -1;
}
if (strncmp(nsec_or_hex, "nsec1", 5) == 0) {
return nostr_decode_nsec(nsec_or_hex, out_private_key) == 0 ? 0 : -1;
}
if (is_hex64(nsec_or_hex)) {
return nostr_hex_to_bytes(nsec_or_hex, out_private_key, 32) == 0 ? 0 : -1;
}
return -1;
}
static int decode_pubkey_hex_or_npub(const char* in, char out_hex[65]) {
if (!in || !out_hex) {
return -1;
}
if (is_hex64(in)) {
memcpy(out_hex, in, 65);
return 0;
}
if (strncmp(in, "npub1", 5) == 0) {
unsigned char pubkey[32];
if (nostr_decode_npub(in, pubkey) != 0) {
return -1;
}
nostr_bytes_to_hex(pubkey, 32, out_hex);
return 0;
}
return -1;
}
static int parse_tools_config(cJSON* root, didactyl_config_t* config) {
cJSON* tools = cJSON_GetObjectItemCaseSensitive(root, "tools");
if (!tools || !cJSON_IsObject(tools)) {
return 0;
}
cJSON* enabled = cJSON_GetObjectItemCaseSensitive(tools, "enabled");
cJSON* max_turns = cJSON_GetObjectItemCaseSensitive(tools, "max_turns");
if (enabled && cJSON_IsBool(enabled)) {
config->tools.enabled = cJSON_IsTrue(enabled) ? 1 : 0;
}
if (max_turns && cJSON_IsNumber(max_turns)) {
config->tools.max_turns = (int)max_turns->valuedouble;
}
cJSON* shell = cJSON_GetObjectItemCaseSensitive(tools, "shell");
if (!shell || !cJSON_IsObject(shell)) {
return 0;
}
cJSON* shell_enabled = cJSON_GetObjectItemCaseSensitive(shell, "enabled");
cJSON* timeout_seconds = cJSON_GetObjectItemCaseSensitive(shell, "timeout_seconds");
cJSON* max_output_bytes = cJSON_GetObjectItemCaseSensitive(shell, "max_output_bytes");
if (shell_enabled && cJSON_IsBool(shell_enabled)) {
config->tools.shell.enabled = cJSON_IsTrue(shell_enabled) ? 1 : 0;
}
if (timeout_seconds && cJSON_IsNumber(timeout_seconds)) {
config->tools.shell.timeout_seconds = (int)timeout_seconds->valuedouble;
}
if (max_output_bytes && cJSON_IsNumber(max_output_bytes)) {
config->tools.shell.max_output_bytes = (int)max_output_bytes->valuedouble;
}
if (copy_json_string(shell,
"working_directory",
config->tools.shell.working_directory,
sizeof(config->tools.shell.working_directory),
0) != 0) {
return -1;
}
return 0;
}
static int parse_security_config(cJSON* root, didactyl_config_t* config) {
cJSON* security = cJSON_GetObjectItemCaseSensitive(root, "security");
if (!security || !cJSON_IsObject(security)) {
return 0;
}
cJSON* verify_signatures = cJSON_GetObjectItemCaseSensitive(security, "verify_signatures");
if (verify_signatures && cJSON_IsBool(verify_signatures)) {
config->security.verify_signatures = cJSON_IsTrue(verify_signatures) ? 1 : 0;
}
if (copy_json_string(security,
"stranger_response",
config->security.stranger_response,
sizeof(config->security.stranger_response),
0) != 0) {
return -1;
}
cJSON* tiers = cJSON_GetObjectItemCaseSensitive(security, "tiers");
if (!tiers || !cJSON_IsObject(tiers)) {
return 0;
}
cJSON* admin_tier = cJSON_GetObjectItemCaseSensitive(tiers, "admin");
if (admin_tier && cJSON_IsObject(admin_tier)) {
cJSON* tools_enabled = cJSON_GetObjectItemCaseSensitive(admin_tier, "tools_enabled");
if (tools_enabled && cJSON_IsBool(tools_enabled)) {
config->security.admin.tools_enabled = cJSON_IsTrue(tools_enabled) ? 1 : 0;
}
}
cJSON* wot_tier = cJSON_GetObjectItemCaseSensitive(tiers, "wot");
if (wot_tier && cJSON_IsObject(wot_tier)) {
cJSON* enabled = cJSON_GetObjectItemCaseSensitive(wot_tier, "enabled");
cJSON* tools_enabled = cJSON_GetObjectItemCaseSensitive(wot_tier, "tools_enabled");
if (enabled && cJSON_IsBool(enabled)) {
config->security.wot.enabled = cJSON_IsTrue(enabled) ? 1 : 0;
}
if (tools_enabled && cJSON_IsBool(tools_enabled)) {
config->security.wot.tools_enabled = cJSON_IsTrue(tools_enabled) ? 1 : 0;
}
}
cJSON* stranger_tier = cJSON_GetObjectItemCaseSensitive(tiers, "stranger");
if (stranger_tier && cJSON_IsObject(stranger_tier)) {
cJSON* enabled = cJSON_GetObjectItemCaseSensitive(stranger_tier, "enabled");
if (enabled && cJSON_IsBool(enabled)) {
config->security.stranger.enabled = cJSON_IsTrue(enabled) ? 1 : 0;
}
}
return 0;
}
static int parse_admin_context_config(cJSON* root, didactyl_config_t* config) {
cJSON* admin_context = cJSON_GetObjectItemCaseSensitive(root, "admin_context");
if (!admin_context || !cJSON_IsObject(admin_context)) {
return 0;
}
cJSON* enabled = cJSON_GetObjectItemCaseSensitive(admin_context, "enabled");
if (enabled && cJSON_IsBool(enabled)) {
config->admin_context.enabled = cJSON_IsTrue(enabled) ? 1 : 0;
}
cJSON* kind_1_limit = cJSON_GetObjectItemCaseSensitive(admin_context, "kind_1_limit");
if (kind_1_limit && cJSON_IsNumber(kind_1_limit)) {
config->admin_context.kind_1_limit = (int)kind_1_limit->valuedouble;
}
cJSON* subscribe_kinds = cJSON_GetObjectItemCaseSensitive(admin_context, "subscribe_kinds");
if (subscribe_kinds && cJSON_IsArray(subscribe_kinds)) {
config->admin_context.track_kind_0 = 0;
config->admin_context.track_kind_3 = 0;
config->admin_context.track_kind_10002 = 0;
config->admin_context.track_kind_1 = 0;
int n = cJSON_GetArraySize(subscribe_kinds);
for (int i = 0; i < n; i++) {
cJSON* k = cJSON_GetArrayItem(subscribe_kinds, i);
if (!k || !cJSON_IsNumber(k)) {
continue;
}
int kind = (int)k->valuedouble;
if (kind == 0) config->admin_context.track_kind_0 = 1;
else if (kind == 3) config->admin_context.track_kind_3 = 1;
else if (kind == 10002) config->admin_context.track_kind_10002 = 1;
else if (kind == 1) config->admin_context.track_kind_1 = 1;
}
}
return 0;
}
static int parse_triggers_config(cJSON* root, didactyl_config_t* config) {
cJSON* triggers = cJSON_GetObjectItemCaseSensitive(root, "triggers");
if (!triggers || !cJSON_IsObject(triggers)) {
return 0;
}
cJSON* enabled = cJSON_GetObjectItemCaseSensitive(triggers, "enabled");
cJSON* max_active = cJSON_GetObjectItemCaseSensitive(triggers, "max_active");
cJSON* cooldown_seconds = cJSON_GetObjectItemCaseSensitive(triggers, "cooldown_seconds");
cJSON* llm_rate_limit = cJSON_GetObjectItemCaseSensitive(triggers, "llm_rate_limit_per_minute");
cJSON* template_rate_limit = cJSON_GetObjectItemCaseSensitive(triggers, "template_rate_limit_per_minute");
if (enabled && cJSON_IsBool(enabled)) {
config->triggers.enabled = cJSON_IsTrue(enabled) ? 1 : 0;
}
if (max_active && cJSON_IsNumber(max_active)) {
config->triggers.max_active = (int)max_active->valuedouble;
}
if (cooldown_seconds && cJSON_IsNumber(cooldown_seconds)) {
config->triggers.cooldown_seconds = (int)cooldown_seconds->valuedouble;
}
if (llm_rate_limit && cJSON_IsNumber(llm_rate_limit)) {
config->triggers.llm_rate_limit_per_minute = (int)llm_rate_limit->valuedouble;
}
if (template_rate_limit && cJSON_IsNumber(template_rate_limit)) {
config->triggers.template_rate_limit_per_minute = (int)template_rate_limit->valuedouble;
}
if (config->triggers.max_active < 1) {
config->triggers.max_active = 1;
}
if (config->triggers.cooldown_seconds < 0) {
config->triggers.cooldown_seconds = 0;
}
if (config->triggers.llm_rate_limit_per_minute < 1) {
config->triggers.llm_rate_limit_per_minute = 1;
}
if (config->triggers.template_rate_limit_per_minute < 1) {
config->triggers.template_rate_limit_per_minute = 1;
}
return 0;
}
static cJSON* find_tag_value_string(cJSON* tags, const char* tag_key) {
if (!tags || !cJSON_IsArray(tags) || !tag_key) {
return NULL;
}
int n = cJSON_GetArraySize(tags);
for (int i = 0; i < n; i++) {
cJSON* tag = cJSON_GetArrayItem(tags, i);
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
continue;
}
cJSON* key = cJSON_GetArrayItem(tag, 0);
cJSON* val = cJSON_GetArrayItem(tag, 1);
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) {
continue;
}
if (strcmp(key->valuestring, tag_key) == 0) {
return val;
}
}
return NULL;
}
static int set_tag_value_string(cJSON* tags, const char* tag_key, const char* tag_value) {
if (!tags || !cJSON_IsArray(tags) || !tag_key || !tag_value || tag_value[0] == '\0') {
return -1;
}
int n = cJSON_GetArraySize(tags);
for (int i = 0; i < n; i++) {
cJSON* tag = cJSON_GetArrayItem(tags, i);
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
continue;
}
cJSON* key = cJSON_GetArrayItem(tag, 0);
cJSON* val = cJSON_GetArrayItem(tag, 1);
if (!key || !val || !cJSON_IsString(key) || !key->valuestring) {
continue;
}
if (strcmp(key->valuestring, tag_key) == 0) {
if (cJSON_IsString(val)) {
if (!cJSON_SetValuestring(val, tag_value)) {
return -1;
}
return 0;
}
cJSON* new_val = cJSON_CreateString(tag_value);
if (!new_val) {
return -1;
}
cJSON_ReplaceItemInArray(tag, 1, new_val);
return 0;
}
}
cJSON* new_tag = cJSON_CreateArray();
if (!new_tag) {
return -1;
}
cJSON_AddItemToArray(new_tag, cJSON_CreateString(tag_key));
cJSON_AddItemToArray(new_tag, cJSON_CreateString(tag_value));
cJSON_AddItemToArray(tags, new_tag);
return 0;
}
static int normalize_skill_d_tag(int event_kind, cJSON* item, cJSON* tags) {
if (!item || !tags || !cJSON_IsArray(tags)) {
return 0;
}
if (event_kind != 31123 && event_kind != 31124) {
return 0;
}
cJSON* d_val = find_tag_value_string(tags, "d");
if (!d_val || !cJSON_IsString(d_val) || !d_val->valuestring) {
return 0;
}
int needs_normalize =
(strcmp(d_val->valuestring, "skill") == 0 || strcmp(d_val->valuestring, "private_skill") == 0);
if (!needs_normalize) {
return 0;
}
const char* slug = NULL;
cJSON* slug_val = find_tag_value_string(tags, "slug");
if (slug_val && cJSON_IsString(slug_val) && slug_val->valuestring && slug_val->valuestring[0] != '\0') {
slug = slug_val->valuestring;
}
if (!slug) {
cJSON* content_fields = cJSON_GetObjectItemCaseSensitive(item, "content_fields");
if (content_fields && cJSON_IsObject(content_fields)) {
cJSON* name = cJSON_GetObjectItemCaseSensitive(content_fields, "name");
if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') {
slug = name->valuestring;
}
}
}
if (!slug) {
return 0;
}
return set_tag_value_string(tags, "d", slug);
}
static int parse_startup_events(cJSON* root, didactyl_config_t* config) {
cJSON* arr = cJSON_GetObjectItemCaseSensitive(root, "startup_events");
if (!arr || !cJSON_IsArray(arr)) {
return 0;
}
int count = cJSON_GetArraySize(arr);
if (count <= 0) return 0;
config->startup_events = (startup_event_t*)calloc((size_t)count, sizeof(startup_event_t));
if (!config->startup_events) return -1;
config->startup_event_count = count;
for (int i = 0; i < count; i++) {
cJSON* item = cJSON_GetArrayItem(arr, i);
if (!item || !cJSON_IsObject(item)) return -1;
cJSON* kind = cJSON_GetObjectItemCaseSensitive(item, "kind");
cJSON* content = cJSON_GetObjectItemCaseSensitive(item, "content");
cJSON* content_fields = cJSON_GetObjectItemCaseSensitive(item, "content_fields");
cJSON* tags = cJSON_GetObjectItemCaseSensitive(item, "tags");
if (!kind || !cJSON_IsNumber(kind)) {
return -1;
}
config->startup_events[i].kind = (int)kind->valuedouble;
if (content_fields) {
if (!cJSON_IsObject(content_fields)) {
return -1;
}
config->startup_events[i].content = cJSON_PrintUnformatted(content_fields);
if (!config->startup_events[i].content) {
return -1;
}
} else {
if (!content || !cJSON_IsString(content) || !content->valuestring) {
return -1;
}
config->startup_events[i].content = strdup(content->valuestring);
if (!config->startup_events[i].content) {
return -1;
}
}
if (tags) {
if (!cJSON_IsArray(tags)) return -1;
if (normalize_skill_d_tag(config->startup_events[i].kind, item, tags) != 0) {
return -1;
}
config->startup_events[i].tags_json = cJSON_PrintUnformatted(tags);
if (!config->startup_events[i].tags_json) return -1;
}
}
return 0;
}
static int parse_relays_from_startup_kind10002(didactyl_config_t* config) {
if (!config || !config->startup_events || config->startup_event_count <= 0) {
return -1;
}
for (int i = 0; i < config->startup_event_count; i++) {
startup_event_t* se = &config->startup_events[i];
if (se->kind != 10002 || !se->tags_json) {
continue;
}
cJSON* tags = cJSON_Parse(se->tags_json);
if (!tags || !cJSON_IsArray(tags)) {
cJSON_Delete(tags);
continue;
}
int tag_count = cJSON_GetArraySize(tags);
int relay_count = 0;
for (int j = 0; j < tag_count; j++) {
cJSON* tag = cJSON_GetArrayItem(tags, j);
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
continue;
}
cJSON* key = cJSON_GetArrayItem(tag, 0);
cJSON* val = cJSON_GetArrayItem(tag, 1);
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) {
continue;
}
if (strcmp(key->valuestring, "r") == 0 && val->valuestring[0] != '\0') {
relay_count++;
}
}
if (relay_count <= 0) {
cJSON_Delete(tags);
continue;
}
config->relays = (char**)calloc((size_t)relay_count, sizeof(char*));
if (!config->relays) {
cJSON_Delete(tags);
return -1;
}
config->relay_count = relay_count;
int out_i = 0;
for (int j = 0; j < tag_count && out_i < relay_count; j++) {
cJSON* tag = cJSON_GetArrayItem(tags, j);
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
continue;
}
cJSON* key = cJSON_GetArrayItem(tag, 0);
cJSON* val = cJSON_GetArrayItem(tag, 1);
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) {
continue;
}
if (strcmp(key->valuestring, "r") == 0 && val->valuestring[0] != '\0') {
config->relays[out_i] = strdup(val->valuestring);
if (!config->relays[out_i]) {
cJSON_Delete(tags);
return -1;
}
out_i++;
}
}
cJSON_Delete(tags);
return 0;
}
return -1;
}
static int parse_relays(cJSON* root, didactyl_config_t* config) {
(void)root;
return parse_relays_from_startup_kind10002(config);
}
void config_free(didactyl_config_t* config) {
if (!config) {
return;
}
if (config->relays) {
for (int i = 0; i < config->relay_count; i++) {
free(config->relays[i]);
}
free(config->relays);
}
if (config->startup_events) {
for (int i = 0; i < config->startup_event_count; i++) {
free(config->startup_events[i].content);
free(config->startup_events[i].tags_json);
}
free(config->startup_events);
}
memset(config, 0, sizeof(*config));
}
int config_load(const char* path, didactyl_config_t* config) {
if (!path || !config) {
config_set_error("config_load called with invalid arguments");
return -1;
}
config_set_error("unknown configuration error");
memset(config, 0, sizeof(*config));
snprintf(config->config_path, sizeof(config->config_path), "%s", path);
config->tools.enabled = 1;
config->tools.max_turns = 8;
config->tools.shell.enabled = 1;
config->tools.shell.timeout_seconds = 30;
config->tools.shell.max_output_bytes = 65536;
strcpy(config->tools.shell.working_directory, ".");
config->security.verify_signatures = 1;
config->security.admin.enabled = 1;
config->security.admin.tools_enabled = 1;
config->security.wot.enabled = 1;
config->security.wot.tools_enabled = 0;
config->security.stranger.enabled = 1;
config->security.stranger.tools_enabled = 0;
config->security.stranger_response[0] = '\0';
config->admin_context.enabled = 1;
config->admin_context.track_kind_0 = 1;
config->admin_context.track_kind_3 = 1;
config->admin_context.track_kind_10002 = 1;
config->admin_context.track_kind_1 = 1;
config->admin_context.kind_1_limit = 10;
config->triggers.enabled = 1;
config->triggers.max_active = 16;
config->triggers.cooldown_seconds = 60;
config->triggers.llm_rate_limit_per_minute = 10;
config->triggers.template_rate_limit_per_minute = 60;
char* json_buf = NULL;
size_t json_len = 0;
if (read_file_to_buffer(path, &json_buf, &json_len) != 0) {
config_set_error("failed to read config file '%s': %s", path, strerror(errno));
return -1;
}
cJSON* root = cJSON_ParseWithLength(json_buf, json_len);
free(json_buf);
if (!root) {
const char* err = cJSON_GetErrorPtr();
config_set_error("invalid JSON in config '%s'%s%s",
path,
err ? " near: " : "",
err ? err : "");
return -1;
}
int rc = -1;
cJSON* keys = cJSON_GetObjectItemCaseSensitive(root, "keys");
cJSON* admin = cJSON_GetObjectItemCaseSensitive(root, "admin");
cJSON* llm = cJSON_GetObjectItemCaseSensitive(root, "llm");
if (!keys || !cJSON_IsObject(keys) ||
!admin || !cJSON_IsObject(admin) ||
!llm || !cJSON_IsObject(llm)) {
config_set_error("config must include object sections: keys, admin, llm");
goto cleanup;
}
if (copy_json_string(keys, "nsec", config->keys.nsec, sizeof(config->keys.nsec), 1) != 0) {
config_set_error("keys.nsec is required and must be a non-empty string");
goto cleanup;
}
char admin_key_raw[OW_MAX_KEY_LEN] = {0};
if (copy_json_string(admin, "pubkey", admin_key_raw, sizeof(admin_key_raw), 1) != 0) {
config_set_error("admin.pubkey is required and must be a non-empty string");
goto cleanup;
}
if (decode_pubkey_hex_or_npub(admin_key_raw, config->admin.pubkey) != 0) {
config_set_error("admin.pubkey must be npub1... or 64-char hex");
goto cleanup;
}
if (parse_startup_events(root, config) != 0) {
config_set_error("invalid startup_events configuration");
goto cleanup;
}
if (parse_relays(root, config) != 0) {
config_set_error("relay configuration is invalid: startup_events must include kind 10002 with non-empty r tags");
goto cleanup;
}
if (copy_json_string(llm, "provider", config->llm.provider, sizeof(config->llm.provider), 0) != 0) {
config_set_error("llm.provider must be a string if provided");
goto cleanup;
}
if (config->llm.provider[0] == '\0') {
strcpy(config->llm.provider, "openai");
}
if (copy_json_string(llm, "api_key", config->llm.api_key, sizeof(config->llm.api_key), 1) != 0) {
config_set_error("llm.api_key is required and must be a string");
goto cleanup;
}
if (copy_json_string(llm, "model", config->llm.model, sizeof(config->llm.model), 1) != 0) {
config_set_error("llm.model is required and must be a string");
goto cleanup;
}
if (copy_json_string(llm, "base_url", config->llm.base_url, sizeof(config->llm.base_url), 1) != 0) {
config_set_error("llm.base_url is required and must be a string");
goto cleanup;
}
cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(llm, "max_tokens");
cJSON* temperature = cJSON_GetObjectItemCaseSensitive(llm, "temperature");
config->llm.max_tokens = (max_tokens && cJSON_IsNumber(max_tokens)) ? (int)max_tokens->valuedouble : 512;
config->llm.temperature = (temperature && cJSON_IsNumber(temperature)) ? temperature->valuedouble : 0.7;
if (parse_tools_config(root, config) != 0) {
config_set_error("invalid tools configuration");
goto cleanup;
}
if (parse_security_config(root, config) != 0) {
config_set_error("invalid security configuration");
goto cleanup;
}
if (parse_admin_context_config(root, config) != 0) {
config_set_error("invalid admin_context configuration");
goto cleanup;
}
if (parse_triggers_config(root, config) != 0) {
config_set_error("invalid triggers configuration");
goto cleanup;
}
if (decode_private_key(config->keys.nsec, config->keys.private_key) != 0) {
config_set_error("keys.nsec must be valid nsec1... or 64-char hex private key");
goto cleanup;
}
if (nostr_ec_public_key_from_private_key(config->keys.private_key, config->keys.public_key) != 0) {
config_set_error("failed to derive public key from keys.nsec");
goto cleanup;
}
nostr_bytes_to_hex(config->keys.public_key, 32, config->keys.public_key_hex);
rc = 0;
cleanup:
cJSON_Delete(root);
if (rc != 0) {
config_free(config);
}
return rc;
}