#define _GNU_SOURCE

/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>

/* from secure_mem.h */


/*
 * Secure memory buffer — mlock'd, zeroized on free.
 * Used for mnemonic phrases, private keys, and any sensitive material.
 */
typedef struct {
    void *data;       /* pointer to locked allocation */
    size_t size;      /* usable size in bytes */
    int locked;       /* 1 if mlock succeeded */
} secure_buf_t;

/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);

/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);

/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);


/* from mnemonic.h */


/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256

/*
 * Mnemonic state — holds the loaded mnemonic in secure memory.
 * Only one mnemonic is active at a time per process.
 */
typedef struct {
    secure_buf_t buf;       /* secure storage for the mnemonic string */
    int loaded;             /* 1 if a mnemonic is currently loaded */
    int word_count;         /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;

/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);

/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
 * Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);

/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);

/* Check if a mnemonic is currently loaded. */
int mnemonic_is_loaded(const mnemonic_state_t *state);

/* Get the mnemonic string (only valid while loaded). Returns NULL if not loaded. */
const char *mnemonic_get_phrase(const mnemonic_state_t *state);

/* Generate a new BIP-39 mnemonic phrase (12/15/18/21/24 words) into out.
 * Returns 0 on success, -1 on invalid arguments or generation failure. */
int mnemonic_generate(int word_count, char *out, size_t out_len);


/* from role_table.h */


/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 256

/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
    PURPOSE_NOSTR = 0,
    PURPOSE_BITCOIN,
    PURPOSE_SSH,
    PURPOSE_AGE,
    PURPOSE_FIPS,
    PURPOSE_UNKNOWN
} role_purpose_t;

/* Curve enum */
typedef enum {
    CURVE_SECP256K1 = 0,
    CURVE_ED25519,
    CURVE_X25519,
    CURVE_UNKNOWN
} role_curve_t;

/* Selector type — how this role's key is addressed */
typedef enum {
    SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
    SELECTOR_ROLE_PATH    /* uses explicit full path */
} role_selector_type_t;

/* A single role entry */
typedef struct {
    char name[ROLE_NAME_MAX];
    char purpose_str[ROLE_PURPOSE_MAX];
    char curve_str[ROLE_CURVE_MAX];
    role_purpose_t purpose;
    role_curve_t curve;
    role_selector_type_t selector_type;
    int nostr_index;                         /* valid if selector_type == SELECTOR_NOSTR_INDEX */
    char role_path[ROLE_PATH_MAX];           /* valid if selector_type == SELECTOR_ROLE_PATH */
    char pubkey_hex[ROLE_PUBKEY_HEX_MAX];    /* filled after derivation, empty until then */
    int derived;                             /* 1 if pubkey_hex has been populated */
} role_entry_t;

/* The role table */
typedef struct {
    role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
    int count;
} role_table_t;

/* Initialize an empty role table */
void role_table_init(role_table_t *table);

/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);

/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);

/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);

/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);

/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);

/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);

/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);

/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);

/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);

/* Register a nostr-index role if missing. Returns 0 on success, -1 on error. */
int role_table_register_nostr_index(role_table_t *table, int nostr_index);


/* from selector.h */


/* Error codes for selector resolution */
#define SELECTOR_OK                 0
#define SELECTOR_ERR_AMBIGUOUS     -1   /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND     -2   /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT    -3   /* no selector given and no "main" role exists */

/* Parsed selector from a request's options object */
typedef struct {
    int has_role;          /* 1 if "role" field was present */
    char role_name[ROLE_NAME_MAX];

    int has_nostr_index;   /* 1 if "nostr_index" field was present */
    int nostr_index;

    int has_role_path;     /* 1 if "role_path" field was present */
    char role_path[ROLE_PATH_MAX];
} selector_request_t;

/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);

/*
 * Resolve a selector request against the role table.
 * On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
 * On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
 */
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);

/*
 * Return a human-readable error string for a selector error code.
 */
const char *selector_strerror(int err);


/* from enforcement.h */


/* Error codes */
#define ENFORCE_OK                0
#define ENFORCE_ERR_PURPOSE      -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE        -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */

/* Known verbs */
#define VERB_SIGN_EVENT     "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT  "nip44_encrypt"
#define VERB_NIP44_DECRYPT  "nip44_decrypt"
#define VERB_NIP04_ENCRYPT  "nip04_encrypt"
#define VERB_NIP04_DECRYPT  "nip04_decrypt"

/*
 * Check whether `verb` is allowed to execute against `role`.
 * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
 *
 * The enforcement rules are:
 *   - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
 *     purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
 *   - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
 */
int enforce_verb_role(const char *verb, const role_entry_t *role);

/*
 * Return a human-readable error string for an enforcement error code.
 */
const char *enforce_strerror(int err);


/* from policy.h */


#define POLICY_MAX_ENTRIES    32
#define POLICY_MAX_VERBS      16
#define POLICY_MAX_ROLES      16
#define POLICY_MAX_PURPOSES   8
#define POLICY_VERB_MAX_LEN   32
#define POLICY_CALLER_MAX_LEN 160

/* Prompt behavior */
typedef enum {
    PROMPT_NEVER = 0,
    PROMPT_FIRST_PER_BOOT,
    PROMPT_EVERY_REQUEST,
    PROMPT_DENY
} prompt_mode_t;

typedef enum {
    POLICY_SOURCE_DEFAULT = 0,   /* the catch-all entry */
    POLICY_SOURCE_PREAPPROVE,    /* from --preapprove CLI flag */
    POLICY_SOURCE_SESSION_GRANT  /* from prompt [a] during session */
} policy_source_t;

/* A single policy entry */
typedef struct {
    char caller[POLICY_CALLER_MAX_LEN];   /* e.g. "uid:1000" or "*" for any */
    char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
    int verb_count;
    char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
    int role_count;
    char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
    int purpose_count;
    prompt_mode_t prompt;
    policy_source_t source;
} policy_entry_t;

/* Policy table */
typedef struct {
    policy_entry_t entries[POLICY_MAX_ENTRIES];
    int count;
} policy_table_t;

/* Policy check result */
#define POLICY_ALLOW               0
#define POLICY_ALLOW_SESSION_VERB  3   /* allow + save session grant for this caller+role+verb */
#define POLICY_ALLOW_SESSION_ALL   4   /* allow + save session grant for this caller+role (all verbs) */
#define POLICY_DENY               -1
#define POLICY_PROMPT             -2   /* would need user confirmation */
#define POLICY_NO_MATCH           -3   /* no policy entry matched (fail-closed = deny) */

/* Initialize policy table */
void policy_table_init(policy_table_t *table);

/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);

/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);

/* Insert an entry before the final catch-all rule. */
int policy_table_insert_before_last(policy_table_t *table, const policy_entry_t *entry);

/*
 * Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
 * Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
 */
int policy_check(const policy_table_t *table, const char *caller_id,
                 const char *verb, const char *role_name, const char *purpose,
                 policy_source_t *out_source);

/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);

/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);


/* from crypto.h */


/* Per-role derived key material (stored in secure memory) */
typedef struct {
    secure_buf_t private_key;  /* 32 bytes, mlock'd */
    unsigned char public_key[32];
    char pubkey_hex[65];       /* 64 hex chars + null */
    char npub[128];            /* bech32 npub */
    int valid;
} derived_key_t;

/* Key store — holds derived keys for all roles */
typedef struct {
    derived_key_t keys[ROLE_TABLE_MAX_ENTRIES];
    int count;
} key_store_t;

/* Derive keys for all roles in the table using the loaded mnemonic.
 * Populates key_store and sets role->pubkey_hex and role->derived for each role.
 * Only derives for roles with purpose=nostr and curve=secp256k1 (for now).
 * Returns number of keys derived, or -1 on error. */
int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic);

/* Derive key for exactly one role index. Returns 0 on success, -1 on error. */
int crypto_derive_one(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic, int role_index);

/* Get the derived private key for a role (by table index). Returns NULL if not derived. */
const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index);

/* Get the derived public key hex for a role. Returns NULL if not derived. */
const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index);

/* Sign a Nostr event. event_json is the unsigned event JSON string.
 * Returns a newly-allocated string containing the signed event JSON, or NULL on error.
 * Caller must free() the returned string. */
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json);

/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);


/* from dispatcher.h */


/* Dispatcher context — holds references to shared state */
typedef struct {
    role_table_t *role_table;
    mnemonic_state_t *mnemonic;
    key_store_t *key_store;
} dispatcher_ctx_t;

/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);

/*
 * Process a JSON-RPC request string and produce a JSON-RPC response string.
 *
 * The caller owns the returned string and must free() it.
 * Returns NULL only on catastrophic allocation failure.
 *
 * Response format on success:
 *   { "id": "...", "result": "..." }
 *
 * Response format on error:
 *   { "id": "...", "error": { "code": <int>, "message": "..." } }
 *
 * Error codes:
 *   -32700  Parse error (invalid JSON)
 *   -32600  Invalid request (missing id/method/params)
 *   -32601  Method not found (unknown verb after enforcement)
 *   -32602  Invalid params
 *   1001    ambiguous_role_selector
 *   1002    role_not_found
 *   1003    no_default_role
 *   1004    purpose_mismatch
 *   1005    curve_mismatch
 *   1006    mnemonic_not_loaded
 */
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);


/* from server.h */


#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE    65536

#define NSIGNER_LISTEN_UNIX   0
#define NSIGNER_LISTEN_STDIO  1
#define NSIGNER_LISTEN_QREXEC 2
#define NSIGNER_LISTEN_TCP    3

#define NSIGNER_AUTH_OFF      0
#define NSIGNER_AUTH_OPTIONAL 1
#define NSIGNER_AUTH_REQUIRED 2

/* Caller identity */
typedef struct {
    uid_t uid;
    gid_t gid;
    pid_t pid;
    int kind;
    char caller_id[POLICY_CALLER_MAX_LEN];     /* "uid:<n>", "qubes:<vm>", or "pubkey:<hex>" */
    char source_qube[64];
    int auth_present;
    char auth_pubkey_hex[65];
    char auth_label[64];
} caller_identity_t;

/* Server context */
typedef struct {
    char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */
    char last_error[256];
    int listen_fd;
    int running;
    int listen_mode;
    int stdio_handled;
    dispatcher_ctx_t *dispatcher;
    policy_table_t *policy;
    int socket_name_explicit;
    int auth_mode;
    int auth_skew_seconds;
    int bridge_source_trusted;  /* when set, unix connections send a qrexec_source preamble */
} server_ctx_t;

/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner").
 * socket_name_explicit should be non-zero when provided via --socket-name override. */
void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit,
                 int listen_mode,
                 int auth_mode,
                 int auth_skew_seconds,
                 dispatcher_ctx_t *dispatcher, policy_table_t *policy);

/* Mark this unix listener as a trusted bridge socket. When set, each
 * incoming connection is expected to send a framed JSON preamble
 * {"qrexec_source":"<vm>"} before the actual request, and the caller
 * identity is composed as qubes:<vm>. */
void server_set_bridge_source_trusted(server_ctx_t *ctx, int enabled);

/* Start listening. Returns 0 on success, -1 on error. */
int server_start(server_ctx_t *ctx);

/* Get human-readable description of last server error. */
const char *server_last_error(const server_ctx_t *ctx);

/* Handle one pending connection (non-blocking). Returns 1 if handled, 0 if nothing pending, -1 on error.
 * activity_cb is called with a description string for the TUI activity log. */
typedef void (*server_activity_cb)(const char *message, void *user_data);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);

/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);

/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);

typedef struct {
    const caller_identity_t *caller;
    const char *method;
    const char *role_name;
    const char *purpose;
    int pending_derivation;
    const char *fips_peer_npub;
    const char *fips_peer_name;
} server_approval_request_t;

typedef int (*server_approval_cb)(const server_approval_request_t *req, void *user_data);
void server_set_approval_cb(server_approval_cb cb, void *user_data);


/* from socket_name.h */


/*
 * Generate random socket name in format: nsigner_<word1>_<word2>
 * Returns 0 on success, -1 on error.
 */
int socket_name_random(char *out, size_t out_len);


/* from main.h */
/*
 * nsigner main header - version information
 *
 * Version macros are auto-updated by increment_and_push.sh.
 */


/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"


/* NSIGNER_HEADERLESS_DECLS_END */

int transport_send_framed(int fd, const char *payload);
int transport_recv_framed(int fd, char **out_payload, size_t max_size);

#include "auth_envelope.h"

#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

static int g_prompt_always_allow = 0;
static int g_noninteractive_prompt_default = -1;
static auth_nonce_cache_t g_auth_nonce_cache;
static int g_auth_nonce_cache_inited = 0;
static server_approval_cb g_approval_cb = NULL;
static void *g_approval_cb_user_data = NULL;

static int caller_id_extract_ipv6(const char *caller_id, char *out_ipv6, size_t out_sz) {
    const char *start;
    const char *end;
    size_t len;

    if (caller_id == NULL || out_ipv6 == NULL || out_sz == 0) {
        return -1;
    }

    if (strncmp(caller_id, "tcp:[", 5) != 0) {
        return -1;
    }

    start = caller_id + 5;
    end = strchr(start, ']');
    if (end == NULL || end[1] != ':') {
        return -1;
    }

    len = (size_t)(end - start);
    if (len == 0 || len >= out_sz) {
        return -1;
    }

    memcpy(out_ipv6, start, len);
    out_ipv6[len] = '\0';
    return 0;
}

static int read_cmd_output(const char *cmd, char **out_buf) {
    FILE *fp;
    char chunk[512];
    char *buf = NULL;
    size_t used = 0;
    size_t cap = 0;

    if (cmd == NULL || out_buf == NULL) {
        return -1;
    }

    *out_buf = NULL;
    fp = popen(cmd, "r");
    if (fp == NULL) {
        return -1;
    }

    while (fgets(chunk, sizeof(chunk), fp) != NULL) {
        size_t n = strlen(chunk);
        if (used + n + 1 > cap) {
            size_t new_cap = (cap == 0) ? 2048 : cap * 2;
            while (new_cap < used + n + 1) {
                new_cap *= 2;
            }
            {
                char *tmp = (char *)realloc(buf, new_cap);
                if (tmp == NULL) {
                    free(buf);
                    (void)pclose(fp);
                    return -1;
                }
                buf = tmp;
                cap = new_cap;
            }
        }
        memcpy(buf + used, chunk, n);
        used += n;
    }

    (void)pclose(fp);

    if (buf == NULL) {
        return -1;
    }

    buf[used] = '\0';
    *out_buf = buf;
    return 0;
}

static int lookup_fips_peer_for_ipv6(const char *ipv6,
                                     char *out_npub,
                                     size_t out_npub_sz,
                                     char *out_name,
                                     size_t out_name_sz) {
    char *json = NULL;
    cJSON *root = NULL;
    cJSON *peers = NULL;
    int i;

    if (ipv6 == NULL || out_npub == NULL || out_npub_sz == 0 || out_name == NULL || out_name_sz == 0) {
        return -1;
    }

    out_npub[0] = '\0';
    out_name[0] = '\0';

    if (read_cmd_output("fipsctl show peers 2>/dev/null", &json) != 0) {
        return -1;
    }

    root = cJSON_Parse(json);
    free(json);
    if (root == NULL) {
        return -1;
    }

    peers = cJSON_GetObjectItemCaseSensitive(root, "peers");
    if (!cJSON_IsArray(peers)) {
        cJSON_Delete(root);
        return -1;
    }

    for (i = 0; i < cJSON_GetArraySize(peers); ++i) {
        cJSON *peer = cJSON_GetArrayItem(peers, i);
        cJSON *peer_ip;
        cJSON *peer_npub;
        cJSON *peer_name;

        if (!cJSON_IsObject(peer)) {
            continue;
        }

        peer_ip = cJSON_GetObjectItemCaseSensitive(peer, "ipv6_addr");
        if (!cJSON_IsString(peer_ip) || peer_ip->valuestring == NULL) {
            continue;
        }

        if (strcmp(peer_ip->valuestring, ipv6) != 0) {
            continue;
        }

        peer_npub = cJSON_GetObjectItemCaseSensitive(peer, "npub");
        peer_name = cJSON_GetObjectItemCaseSensitive(peer, "display_name");

        if (cJSON_IsString(peer_npub) && peer_npub->valuestring != NULL) {
            strncpy(out_npub, peer_npub->valuestring, out_npub_sz - 1);
            out_npub[out_npub_sz - 1] = '\0';
        }
        if (cJSON_IsString(peer_name) && peer_name->valuestring != NULL) {
            strncpy(out_name, peer_name->valuestring, out_name_sz - 1);
            out_name[out_name_sz - 1] = '\0';
        }

        cJSON_Delete(root);
        return (out_npub[0] != '\0') ? 0 : -1;
    }

    cJSON_Delete(root);
    return -1;
}

void server_set_prompt_always_allow(int enabled) {
    g_prompt_always_allow = enabled ? 1 : 0;
}

void server_set_noninteractive_prompt_default(int decision) {
    if (decision == POLICY_ALLOW || decision == POLICY_DENY) {
        g_noninteractive_prompt_default = decision;
    } else {
        g_noninteractive_prompt_default = -1;
    }
}

void server_set_approval_cb(server_approval_cb cb, void *user_data) {
    g_approval_cb = cb;
    g_approval_cb_user_data = user_data;
}

static int prompt_for_policy_decision(const caller_identity_t *caller,
                                      const char *method,
                                      const char *role_name,
                                      const char *purpose,
                                      int pending_derivation) {
    int ch;

    if (g_prompt_always_allow) {
        return POLICY_ALLOW;
    }

    if (!isatty(STDIN_FILENO)) {
        if (g_noninteractive_prompt_default == POLICY_ALLOW ||
            g_noninteractive_prompt_default == POLICY_DENY) {
            return g_noninteractive_prompt_default;
        }
        return POLICY_DENY;
    }

    if (g_approval_cb != NULL) {
        server_approval_request_t req;
        char ipv6[INET6_ADDRSTRLEN];
        char npub[128];
        char display_name[128];

        req.caller = caller;
        req.method = method;
        req.role_name = role_name;
        req.purpose = purpose;
        req.pending_derivation = pending_derivation;
        req.fips_peer_npub = NULL;
        req.fips_peer_name = NULL;

        npub[0] = '\0';
        display_name[0] = '\0';
        if (caller != NULL && caller->kind == NSIGNER_LISTEN_TCP &&
            caller_id_extract_ipv6(caller->caller_id, ipv6, sizeof(ipv6)) == 0 &&
            lookup_fips_peer_for_ipv6(ipv6, npub, sizeof(npub), display_name, sizeof(display_name)) == 0) {
            req.fips_peer_npub = (npub[0] != '\0') ? npub : NULL;
            req.fips_peer_name = (display_name[0] != '\0') ? display_name : NULL;
        }

        return g_approval_cb(&req, g_approval_cb_user_data);
    }

    printf("\nApproval required\n");
    printf("caller: %s\n", (caller != NULL) ? caller->caller_id : "unknown");
    if (caller != NULL && caller->kind == NSIGNER_LISTEN_TCP) {
        char ipv6[INET6_ADDRSTRLEN];
        char npub[128];
        char display_name[128];

        npub[0] = '\0';
        display_name[0] = '\0';

        if (caller_id_extract_ipv6(caller->caller_id, ipv6, sizeof(ipv6)) == 0 &&
            lookup_fips_peer_for_ipv6(ipv6, npub, sizeof(npub), display_name, sizeof(display_name)) == 0) {
            if (display_name[0] != '\0') {
                printf("fips peer: %s (%s)\n", npub, display_name);
            } else {
                printf("fips peer: %s\n", npub);
            }
        }
    }

    printf("method: %s\n", (method != NULL) ? method : "unknown");
    printf("role: %s\n", (role_name != NULL) ? role_name : "unknown");
    printf("purpose: %s\n", (purpose != NULL) ? purpose : "unknown");
    if (pending_derivation) {
        printf("  ** NEW IDENTITY — will be derived if approved **\n");
    }
    printf("[y] allow once  [n] deny  [e] allow this caller+role+verb for session\n");
    printf("[a] allow this caller+role for session (all verbs)\n> ");
    fflush(stdout);

    ch = getchar();
    while (ch != EOF && ch != '\n') {
        int next = getchar();
        if (next == EOF || next == '\n') {
            break;
        }
    }

    ch = tolower(ch);
    if (ch == 'a') {
        return POLICY_ALLOW_SESSION_ALL;
    }
    if (ch == 'e') {
        return POLICY_ALLOW_SESSION_VERB;
    }
    if (ch == 'y') {
        return POLICY_ALLOW;
    }
    return POLICY_DENY;
}

static void server_set_error(server_ctx_t *ctx, const char *msg) {
    if (ctx == NULL) {
        return;
    }

    if (msg == NULL) {
        ctx->last_error[0] = '\0';
        return;
    }

    strncpy(ctx->last_error, msg, sizeof(ctx->last_error) - 1);
    ctx->last_error[sizeof(ctx->last_error) - 1] = '\0';
}


static int parse_tcp_target(const char *target,
                            int *out_family,
                            char *out_host,
                            size_t out_host_sz,
                            uint16_t *out_port) {
    const char *p;
    const char *host_start;
    const char *host_end;
    const char *port_start;
    char port_buf[16];
    size_t host_len;
    size_t port_len;
    char *endptr = NULL;
    long port_long;

    if (target == NULL || out_family == NULL || out_host == NULL || out_port == NULL ||
        out_host_sz == 0) {
        return -1;
    }

    if (strncmp(target, "tcp:", 4) != 0) {
        return -1;
    }

    p = target + 4;
    if (*p == '[') {
        host_start = p + 1;
        host_end = strchr(host_start, ']');
        if (host_end == NULL || host_end[1] != ':') {
            return -1;
        }
        port_start = host_end + 2;
    } else {
        host_start = p;
        host_end = strrchr(p, ':');
        if (host_end == NULL || host_end == host_start) {
            return -1;
        }
        port_start = host_end + 1;
    }

    host_len = (size_t)(host_end - host_start);
    if (host_len == 0 || host_len >= out_host_sz) {
        return -1;
    }
    memcpy(out_host, host_start, host_len);
    out_host[host_len] = '\0';

    port_len = strlen(port_start);
    if (port_len == 0 || port_len >= sizeof(port_buf)) {
        return -1;
    }
    memcpy(port_buf, port_start, port_len + 1);

    errno = 0;
    port_long = strtol(port_buf, &endptr, 10);
    if (errno != 0 || endptr == port_buf || *endptr != '\0' || port_long < 1 || port_long > 65535) {
        return -1;
    }

    {
        struct in6_addr addr6;
        struct in_addr addr4;

        if (inet_pton(AF_INET6, out_host, &addr6) == 1) {
            *out_family = AF_INET6;
        } else if (inet_pton(AF_INET, out_host, &addr4) == 1) {
            *out_family = AF_INET;
        } else {
            return -1;
        }
    }

    *out_port = (uint16_t)port_long;
    return 0;
}

static void json_copy_string(char *dst, size_t dst_sz, const char *src, const char *fallback) {
    const char *s = (src != NULL) ? src : fallback;

    if (dst == NULL || dst_sz == 0) {
        return;
    }

    if (s == NULL) {
        dst[0] = '\0';
        return;
    }

    strncpy(dst, s, dst_sz - 1);
    dst[dst_sz - 1] = '\0';
}

static int extract_request_id_compact(const char *json, char *out_id, size_t out_id_sz) {
    cJSON *root;
    cJSON *id_item;
    char *printed = NULL;

    if (json == NULL || out_id == NULL || out_id_sz == 0) {
        return -1;
    }

    out_id[0] = '\0';
    root = cJSON_Parse(json);
    if (root == NULL) {
        return -1;
    }

    id_item = cJSON_GetObjectItemCaseSensitive(root, "id");
    if (id_item == NULL) {
        cJSON_Delete(root);
        return -1;
    }

    if (cJSON_IsString(id_item) && id_item->valuestring != NULL) {
        json_copy_string(out_id, out_id_sz, id_item->valuestring, "null");
        cJSON_Delete(root);
        return 0;
    }

    printed = cJSON_PrintUnformatted(id_item);
    if (printed != NULL) {
        json_copy_string(out_id, out_id_sz, printed, "null");
        free(printed);
        cJSON_Delete(root);
        return 0;
    }

    cJSON_Delete(root);
    return -1;
}

static int request_has_auth_field(const char *json) {
    cJSON *root;
    cJSON *auth;
    int has_auth;

    if (json == NULL) {
        return 0;
    }

    root = cJSON_Parse(json);
    if (root == NULL) {
        return 0;
    }

    auth = cJSON_GetObjectItemCaseSensitive(root, "auth");
    has_auth = (auth != NULL) ? 1 : 0;
    cJSON_Delete(root);
    return has_auth;
}

static void caller_id_compose_with_pubkey(char *out,
                                          size_t out_sz,
                                          int caller_kind,
                                          const char *source_qube,
                                          const char *pubkey_hex) {
    if (out == NULL || out_sz == 0 || pubkey_hex == NULL) {
        return;
    }

    if (caller_kind == NSIGNER_LISTEN_QREXEC && source_qube != NULL && source_qube[0] != '\0') {
        (void)snprintf(out, out_sz, "qubes:%s+pubkey:%s", source_qube, pubkey_hex);
        return;
    }

    (void)snprintf(out, out_sz, "pubkey:%s", pubkey_hex);
}

static int extract_method_and_selector(const char *json,
                                       char *method,
                                       size_t method_sz,
                                       selector_request_t *selector_req) {
    cJSON *root;
    cJSON *method_item;
    cJSON *params_item;
    cJSON *options_item;
    cJSON *tmp;

    if (json == NULL || method == NULL || selector_req == NULL) {
        return -1;
    }

    method[0] = '\0';
    selector_request_init(selector_req);

    root = cJSON_Parse(json);
    if (root == NULL) {
        return -1;
    }

    method_item = cJSON_GetObjectItemCaseSensitive(root, "method");
    if (!cJSON_IsString(method_item) || method_item->valuestring == NULL) {
        cJSON_Delete(root);
        return -1;
    }

    json_copy_string(method, method_sz, method_item->valuestring, "unknown");

    params_item = cJSON_GetObjectItemCaseSensitive(root, "params");
    if (cJSON_IsArray(params_item)) {
        {
            int params_count = cJSON_GetArraySize(params_item);
            options_item = (params_count > 0) ? cJSON_GetArrayItem(params_item, params_count - 1) : NULL;
        }
        if (cJSON_IsObject(options_item)) {
            tmp = cJSON_GetObjectItemCaseSensitive(options_item, "role");
            if (cJSON_IsString(tmp) && tmp->valuestring != NULL) {
                selector_req->has_role = 1;
                json_copy_string(selector_req->role_name, sizeof(selector_req->role_name), tmp->valuestring, "");
            }

            tmp = cJSON_GetObjectItemCaseSensitive(options_item, "nostr_index");
            if (cJSON_IsNumber(tmp)) {
                selector_req->has_nostr_index = 1;
                selector_req->nostr_index = tmp->valueint;
            }

            tmp = cJSON_GetObjectItemCaseSensitive(options_item, "role_path");
            if (cJSON_IsString(tmp) && tmp->valuestring != NULL) {
                selector_req->has_role_path = 1;
                json_copy_string(selector_req->role_path, sizeof(selector_req->role_path), tmp->valuestring, "");
            }
        }
    }

    cJSON_Delete(root);
    return 0;
}

void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit,
                 int listen_mode,
                 int auth_mode,
                 int auth_skew_seconds,
                 dispatcher_ctx_t *dispatcher, policy_table_t *policy) {
    if (ctx == NULL) {
        return;
    }

    memset(ctx, 0, sizeof(*ctx));
    ctx->last_error[0] = '\0';
    if (socket_name != NULL) {
        strncpy(ctx->socket_name, socket_name, sizeof(ctx->socket_name) - 1);
        ctx->socket_name[sizeof(ctx->socket_name) - 1] = '\0';
    }
    ctx->listen_fd = -1;
    ctx->listen_mode = listen_mode;
    ctx->stdio_handled = 0;
    ctx->dispatcher = dispatcher;
    ctx->policy = policy;
    ctx->socket_name_explicit = socket_name_explicit ? 1 : 0;
    ctx->auth_mode = auth_mode;
    ctx->auth_skew_seconds = (auth_skew_seconds > 0) ? auth_skew_seconds : AUTH_DEFAULT_SKEW_SECONDS;
    ctx->bridge_source_trusted = 0;
    if (!g_auth_nonce_cache_inited) {
        auth_nonce_cache_init(&g_auth_nonce_cache);
        g_auth_nonce_cache_inited = 1;
    }
}

void server_set_bridge_source_trusted(server_ctx_t *ctx, int enabled) {
    if (ctx == NULL) {
        return;
    }
    ctx->bridge_source_trusted = enabled ? 1 : 0;
}

int server_start(server_ctx_t *ctx) {
    int fd;
    struct sockaddr_un addr;
    socklen_t addr_len;
    int flags;
    int attempt;

    if (ctx == NULL) {
        return -1;
    }

    if (ctx->dispatcher == NULL || ctx->policy == NULL || ctx->socket_name[0] == '\0') {
        server_set_error(ctx, "invalid server context (missing dispatcher/policy/socket_name)");
        return -1;
    }

    server_set_error(ctx, NULL);

    if (ctx->listen_mode == NSIGNER_LISTEN_STDIO || ctx->listen_mode == NSIGNER_LISTEN_QREXEC) {
        ctx->listen_fd = STDIN_FILENO;
        ctx->running = 1;
        ctx->stdio_handled = 0;
        server_set_error(ctx, NULL);
        return 0;
    }

    if (ctx->listen_mode == NSIGNER_LISTEN_TCP) {
        int family;
        uint16_t port;
        char host[64];
        int one = 1;

        int prc = parse_tcp_target(ctx->socket_name, &family, host, sizeof(host), &port);
        if (prc != 0) {
            (void)snprintf(ctx->last_error,
                           sizeof(ctx->last_error),
                           "invalid tcp listen target: %s (expected tcp:IPv4:PORT or tcp:[IPv6]:PORT)",
                           ctx->socket_name);
            return -1;
        }

        fd = socket(family, SOCK_STREAM, 0);
        if (fd < 0) {
            (void)snprintf(ctx->last_error,
                           sizeof(ctx->last_error),
                           "socket(tcp) failed: %s",
                           strerror(errno));
            return -1;
        }

        (void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));

        if (family == AF_INET) {
            struct sockaddr_in addr4;
            memset(&addr4, 0, sizeof(addr4));
            addr4.sin_family = AF_INET;
            addr4.sin_port = htons(port);
            if (inet_pton(AF_INET, host, &addr4.sin_addr) != 1) {
                close(fd);
                server_set_error(ctx, "inet_pton(AF_INET) failed for listen target");
                return -1;
            }
            if (bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)) != 0) {
                (void)snprintf(ctx->last_error,
                               sizeof(ctx->last_error),
                               "bind(%s) failed: %s",
                               ctx->socket_name,
                               strerror(errno));
                close(fd);
                return -1;
            }
        } else {
            struct sockaddr_in6 addr6;
            memset(&addr6, 0, sizeof(addr6));
            addr6.sin6_family = AF_INET6;
            addr6.sin6_port = htons(port);
            if (inet_pton(AF_INET6, host, &addr6.sin6_addr) != 1) {
                close(fd);
                server_set_error(ctx, "inet_pton(AF_INET6) failed for listen target");
                return -1;
            }
            if (bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)) != 0) {
                (void)snprintf(ctx->last_error,
                               sizeof(ctx->last_error),
                               "bind(%s) failed: %s",
                               ctx->socket_name,
                               strerror(errno));
                close(fd);
                return -1;
            }
        }

        if (listen(fd, 16) != 0) {
            (void)snprintf(ctx->last_error,
                           sizeof(ctx->last_error),
                           "listen() failed: %s",
                           strerror(errno));
            close(fd);
            return -1;
        }

        flags = fcntl(fd, F_GETFL, 0);
        if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) {
            (void)snprintf(ctx->last_error,
                           sizeof(ctx->last_error),
                           "fcntl(O_NONBLOCK) failed: %s",
                           strerror(errno));
            close(fd);
            return -1;
        }

        ctx->listen_fd = fd;
        ctx->running = 1;
        server_set_error(ctx, NULL);
        return 0;
    }

    fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (fd < 0) {
        (void)snprintf(ctx->last_error,
                       sizeof(ctx->last_error),
                       "socket(AF_UNIX, SOCK_STREAM) failed: %s",
                       strerror(errno));
        return -1;
    }

    for (attempt = 0; attempt < 8; ++attempt) {
        memset(&addr, 0, sizeof(addr));
        addr.sun_family = AF_UNIX;
        addr.sun_path[0] = '\0';
        strncpy(&addr.sun_path[1], ctx->socket_name, sizeof(addr.sun_path) - 2);
        addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';

        addr_len = (socklen_t)(sizeof(sa_family_t) + 1 + strlen(ctx->socket_name));

        if (bind(fd, (struct sockaddr *)&addr, addr_len) == 0) {
            break;
        }

        if (errno == EADDRINUSE && !ctx->socket_name_explicit) {
            if (socket_name_random(ctx->socket_name, sizeof(ctx->socket_name)) == 0) {
                continue;
            }
            (void)snprintf(ctx->last_error,
                           sizeof(ctx->last_error),
                           "bind(@%s) failed: %s (and failed to generate retry socket name)",
                           ctx->socket_name,
                           strerror(errno));
            close(fd);
            return -1;
        }

        if (errno == EADDRINUSE && ctx->socket_name_explicit) {
            (void)snprintf(ctx->last_error,
                           sizeof(ctx->last_error),
                           "bind(@%s) failed: %s (explicit --socket-name is already in use)",
                           ctx->socket_name,
                           strerror(errno));
        } else {
            (void)snprintf(ctx->last_error,
                           sizeof(ctx->last_error),
                           "bind(@%s) failed: %s",
                           ctx->socket_name,
                           strerror(errno));
        }
        close(fd);
        return -1;
    }

    if (attempt >= 8) {
        (void)snprintf(ctx->last_error,
                       sizeof(ctx->last_error),
                       "bind failed after retries: address already in use for generated names (try --socket-name)");
        close(fd);
        return -1;
    }

    if (listen(fd, 5) != 0) {
        (void)snprintf(ctx->last_error,
                       sizeof(ctx->last_error),
                       "listen() failed: %s",
                       strerror(errno));
        close(fd);
        return -1;
    }

    flags = fcntl(fd, F_GETFL, 0);
    if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) {
        (void)snprintf(ctx->last_error,
                       sizeof(ctx->last_error),
                       "fcntl(O_NONBLOCK) failed: %s",
                       strerror(errno));
        close(fd);
        return -1;
    }

    ctx->listen_fd = fd;
    ctx->running = 1;
    server_set_error(ctx, NULL);
    return 0;
}

const char *server_last_error(const server_ctx_t *ctx) {
    if (ctx == NULL || ctx->last_error[0] == '\0') {
        return "unknown server error";
    }

    return ctx->last_error;
}

int server_get_caller(int fd, caller_identity_t *out) {
    struct ucred cred;
    socklen_t len = sizeof(cred);

    if (out == NULL) {
        return -1;
    }

    memset(out, 0, sizeof(*out));

    if (fd == STDIN_FILENO) {
        const char *src = getenv("QREXEC_REMOTE_DOMAIN");
        out->kind = NSIGNER_LISTEN_STDIO;
        if (src != NULL && src[0] != '\0') {
            out->kind = NSIGNER_LISTEN_QREXEC;
            strncpy(out->source_qube, src, sizeof(out->source_qube) - 1);
            out->source_qube[sizeof(out->source_qube) - 1] = '\0';
            (void)snprintf(out->caller_id, sizeof(out->caller_id), "qubes:%.57s", out->source_qube);
            return 0;
        }

        out->uid = getuid();
        out->gid = getgid();
        out->pid = getpid();
        (void)snprintf(out->caller_id, sizeof(out->caller_id), "uid:%u", (unsigned int)out->uid);
        return 0;
    }

    {
        struct sockaddr_storage peer;
        socklen_t peer_len = sizeof(peer);
        if (getpeername(fd, (struct sockaddr *)&peer, &peer_len) == 0) {
            if (peer.ss_family == AF_INET) {
                const struct sockaddr_in *in4 = (const struct sockaddr_in *)&peer;
                char ip[INET_ADDRSTRLEN];
                if (inet_ntop(AF_INET, &in4->sin_addr, ip, sizeof(ip)) != NULL) {
                    out->kind = NSIGNER_LISTEN_TCP;
                    (void)snprintf(out->caller_id,
                                   sizeof(out->caller_id),
                                   "tcp:%s:%u",
                                   ip,
                                   (unsigned int)ntohs(in4->sin_port));
                    return 0;
                }
            } else if (peer.ss_family == AF_INET6) {
                const struct sockaddr_in6 *in6 = (const struct sockaddr_in6 *)&peer;
                char ip6[INET6_ADDRSTRLEN];
                if (inet_ntop(AF_INET6, &in6->sin6_addr, ip6, sizeof(ip6)) != NULL) {
                    out->kind = NSIGNER_LISTEN_TCP;
                    (void)snprintf(out->caller_id,
                                   sizeof(out->caller_id),
                                   "tcp:[%s]:%u",
                                   ip6,
                                   (unsigned int)ntohs(in6->sin6_port));
                    return 0;
                }
            }
        }
    }

    if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) != 0) {
        return -1;
    }

    out->uid = cred.uid;
    out->gid = cred.gid;
    out->pid = cred.pid;
    out->kind = NSIGNER_LISTEN_UNIX;
    (void)snprintf(out->caller_id, sizeof(out->caller_id), "uid:%u", (unsigned int)out->uid);
    return 0;
}

int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
    int client_fd;
    caller_identity_t caller;
    char *request = NULL;
    char *response = NULL;
    char method[64];
    char request_id[128];
    char auth_pubkey[65];
    char auth_label[64];
    char role_name[ROLE_NAME_MAX];
    char purpose[ROLE_PURPOSE_MAX];
    selector_request_t selector_req;
    role_entry_t *role = NULL;
    int selector_rc;
    int pchk;
    policy_source_t policy_src = POLICY_SOURCE_DEFAULT;
    int pending_derivation = 0;
    int hard_selector_error = 0;
    int derivation_error = 0;
    char activity[256];
    const char *verdict = "DENIED";
    const char *source_label = "no-match";
    int auth_err_code = 0;
    const char *auth_err_msg = NULL;

    if (ctx == NULL || ctx->listen_fd < 0 || ctx->dispatcher == NULL || ctx->policy == NULL) {
        return -1;
    }

    if (ctx->listen_mode == NSIGNER_LISTEN_STDIO || ctx->listen_mode == NSIGNER_LISTEN_QREXEC) {
        if (ctx->stdio_handled) {
            return 0;
        }
        client_fd = STDIN_FILENO;
        ctx->stdio_handled = 1;
    } else {
        client_fd = accept(ctx->listen_fd, NULL, NULL);
        if (client_fd < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                return 0;
            }
            return -1;
        }
    }

    if (server_get_caller(client_fd, &caller) != 0) {
        if (client_fd != STDIN_FILENO) {
            close(client_fd);
        }
        return -1;
    }

    /*
     * Bridge-source-trusted path: when the unix listener is marked as a
     * trusted bridge socket (started with --bridge-source-trusted), each
     * connection sends a framed JSON preamble {"qrexec_source":"<vm>"}
     * before the actual request. We parse it and override the caller
     * identity to qubes:<vm>, matching the native qrexec identity path.
     *
     * Trust rationale: the bridge socket is an abstract unix socket
     * inside the signer qube, reachable only by processes already inside
     * that qube. The qrexec framework authenticated the source qube and
     * set QREXEC_REMOTE_DOMAIN in the bridge's environment; the bridge
     * relays that framework-vouched value to us.
     */
    if (ctx->bridge_source_trusted &&
        ctx->listen_mode == NSIGNER_LISTEN_UNIX &&
        client_fd != STDIN_FILENO) {
        char *preamble = NULL;
        cJSON *proot = NULL;
        cJSON *src_item;

        if (transport_recv_framed(client_fd, &preamble, 4096) != 0) {
            response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32700,\"message\":\"bridge_preamble_missing\"}}");
            if (response != NULL) {
                (void)transport_send_framed(client_fd, response);
                free(response);
            }
            close(client_fd);
            return 1;
        }

        proot = cJSON_Parse(preamble);
        free(preamble);

        if (proot == NULL) {
            response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32700,\"message\":\"bridge_preamble_parse_error\"}}");
            if (response != NULL) {
                (void)transport_send_framed(client_fd, response);
                free(response);
            }
            close(client_fd);
            return 1;
        }

        src_item = cJSON_GetObjectItemCaseSensitive(proot, "qrexec_source");
        if (cJSON_IsString(src_item) && src_item->valuestring != NULL &&
            src_item->valuestring[0] != '\0') {
            caller.kind = NSIGNER_LISTEN_QREXEC;
            strncpy(caller.source_qube, src_item->valuestring, sizeof(caller.source_qube) - 1);
            caller.source_qube[sizeof(caller.source_qube) - 1] = '\0';
            (void)snprintf(caller.caller_id, sizeof(caller.caller_id),
                           "qubes:%.57s", caller.source_qube);
        }
        cJSON_Delete(proot);
    }

    if (transport_recv_framed(client_fd, &request, SERVER_MAX_MSG_SIZE) != 0) {
        response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32700,\"message\":\"parse_error\"}}");
        if (response != NULL) {
            (void)transport_send_framed((client_fd == STDIN_FILENO) ? STDOUT_FILENO : client_fd, response);
            free(response);
        }
        if (client_fd != STDIN_FILENO) {
            close(client_fd);
        }
        return 1;
    }

    json_copy_string(method, sizeof(method), "unknown", "unknown");
    json_copy_string(role_name, sizeof(role_name), "unknown", "unknown");
    json_copy_string(purpose, sizeof(purpose), "unknown", "unknown");
    json_copy_string(request_id, sizeof(request_id), "null", "null");
    auth_pubkey[0] = '\0';
    auth_label[0] = '\0';

    {
        int auth_required = 0;
        int auth_optional = 0;
        int has_auth_field = 0;

        if (caller.kind == NSIGNER_LISTEN_TCP) {
            auth_required = 1;
        } else if (caller.kind == NSIGNER_LISTEN_QREXEC) {
            if (ctx->auth_mode == NSIGNER_AUTH_REQUIRED) {
                auth_required = 1;
            } else if (ctx->auth_mode == NSIGNER_AUTH_OPTIONAL) {
                auth_optional = 1;
            }
        }

        has_auth_field = request_has_auth_field(request);
        if (auth_required || (auth_optional && has_auth_field)) {
            if (auth_envelope_verify_request(request,
                                             &g_auth_nonce_cache,
                                             ctx->auth_skew_seconds,
                                             auth_pubkey,
                                             sizeof(auth_pubkey),
                                             auth_label,
                                             sizeof(auth_label),
                                             &auth_err_code,
                                             &auth_err_msg) != 0) {
                (void)extract_request_id_compact(request, request_id, sizeof(request_id));
                if (auth_err_msg == NULL) {
                    auth_err_msg = "auth_envelope_malformed";
                }
                {
                    char errbuf[256];
                    (void)snprintf(errbuf,
                                   sizeof(errbuf),
                                   "{\"id\":\"%s\",\"error\":{\"code\":%d,\"message\":\"%s\"}}",
                                   request_id,
                                   (auth_err_code != 0) ? auth_err_code : AUTH_ERR_ENVELOPE_MALFORMED,
                                   auth_err_msg);
                    response = strdup(errbuf);
                }
                if (response != NULL) {
                    (void)transport_send_framed((client_fd == STDIN_FILENO) ? STDOUT_FILENO : client_fd, response);
                    free(response);
                }
                free(request);
                if (client_fd != STDIN_FILENO) {
                    close(client_fd);
                }
                return 1;
            }

            caller.auth_present = 1;
            json_copy_string(caller.auth_pubkey_hex,
                             sizeof(caller.auth_pubkey_hex),
                             auth_pubkey,
                             "");
            json_copy_string(caller.auth_label,
                             sizeof(caller.auth_label),
                             auth_label,
                             "");
            caller_id_compose_with_pubkey(caller.caller_id,
                                          sizeof(caller.caller_id),
                                          caller.kind,
                                          caller.source_qube,
                                          auth_pubkey);
        }
    }

    if (extract_method_and_selector(request, method, sizeof(method), &selector_req) == 0) {
        if (ctx->dispatcher->role_table != NULL) {
            selector_rc = selector_resolve(&selector_req, ctx->dispatcher->role_table, &role);
            if (selector_rc == SELECTOR_OK && role != NULL) {
                json_copy_string(role_name, sizeof(role_name), role->name, "main");
                json_copy_string(purpose, sizeof(purpose), role_purpose_to_str(role->purpose), "nostr");
            } else if (selector_rc == SELECTOR_ERR_NOT_FOUND && selector_req.has_nostr_index) {
                pending_derivation = 1;
                if (selector_req.nostr_index == 0) {
                    json_copy_string(role_name, sizeof(role_name), "main", "main");
                } else {
                    (void)snprintf(role_name, sizeof(role_name), "nostr_idx_%d", selector_req.nostr_index);
                }
                json_copy_string(purpose, sizeof(purpose), "nostr", "nostr");
            } else if (selector_rc == SELECTOR_ERR_AMBIGUOUS ||
                       selector_rc == SELECTOR_ERR_NOT_FOUND ||
                       selector_rc == SELECTOR_ERR_NO_DEFAULT) {
                hard_selector_error = selector_rc;
            }
        }
    }

    if (hard_selector_error == SELECTOR_ERR_AMBIGUOUS) {
        response = strdup("{\"id\":\"null\",\"error\":{\"code\":1001,\"message\":\"ambiguous_role_selector\"}}");
        pchk = POLICY_DENY;
    } else if (hard_selector_error == SELECTOR_ERR_NO_DEFAULT) {
        response = strdup("{\"id\":\"null\",\"error\":{\"code\":1003,\"message\":\"no_default_role\"}}");
        pchk = POLICY_DENY;
    } else if (hard_selector_error == SELECTOR_ERR_NOT_FOUND) {
        response = strdup("{\"id\":\"null\",\"error\":{\"code\":1002,\"message\":\"unknown_role\"}}");
        pchk = POLICY_DENY;
    } else {
        pchk = policy_check(ctx->policy, caller.caller_id, method, role_name, purpose, &policy_src);
    }

    if (pchk == POLICY_PROMPT) {
        pchk = prompt_for_policy_decision(&caller, method, role_name, purpose, pending_derivation);
        if (pchk == POLICY_ALLOW_SESSION_VERB || pchk == POLICY_ALLOW_SESSION_ALL) {
            policy_entry_t grant;

            memset(&grant, 0, sizeof(grant));
            strncpy(grant.caller, caller.caller_id, sizeof(grant.caller) - 1);
            strncpy(grant.roles[0], role_name, ROLE_NAME_MAX - 1);
            grant.role_count = 1;
            if (pchk == POLICY_ALLOW_SESSION_VERB) {
                strncpy(grant.verbs[0], method, POLICY_VERB_MAX_LEN - 1);
                grant.verb_count = 1;
            }
            grant.prompt = PROMPT_NEVER;
            grant.source = POLICY_SOURCE_SESSION_GRANT;
            if (policy_table_insert_before_last(ctx->policy, &grant) != 0) {
                pchk = POLICY_DENY;
            } else {
                pchk = POLICY_ALLOW;
            }
        }
    }

    if (pchk == POLICY_ALLOW && pending_derivation) {
        role_entry_t *new_role;
        int role_index;

        if (ctx->dispatcher == NULL ||
            ctx->dispatcher->role_table == NULL ||
            ctx->dispatcher->key_store == NULL ||
            ctx->dispatcher->mnemonic == NULL ||
            role_table_register_nostr_index(ctx->dispatcher->role_table, selector_req.nostr_index) != 0) {
            derivation_error = 1;
        } else {
            new_role = role_table_find_by_nostr_index(ctx->dispatcher->role_table, selector_req.nostr_index);
            if (new_role == NULL) {
                derivation_error = 1;
            } else {
                role_index = (int)(new_role - &ctx->dispatcher->role_table->entries[0]);
                if (role_index < 0 || role_index >= ctx->dispatcher->role_table->count ||
                    crypto_derive_one(ctx->dispatcher->key_store,
                                      ctx->dispatcher->role_table,
                                      ctx->dispatcher->mnemonic,
                                      role_index) != 0) {
                    derivation_error = 1;
                } else {
                    json_copy_string(role_name, sizeof(role_name), new_role->name, role_name);
                    json_copy_string(purpose, sizeof(purpose), role_purpose_to_str(new_role->purpose), "nostr");
                }
            }
        }
    }

    if (pchk == POLICY_ALLOW && !derivation_error) {
        verdict = "ALLOWED";
        response = dispatcher_handle_request(ctx->dispatcher, request);
        if (response == NULL) {
            response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32603,\"message\":\"internal_error\"}}");
        }
    } else if (pchk == POLICY_ALLOW && derivation_error) {
        verdict = "DENIED";
        response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32603,\"message\":\"internal_error\"}}");
    } else {
        verdict = "DENIED";
        if (response == NULL) {
            response = strdup("{\"id\":\"null\",\"error\":{\"code\":2001,\"message\":\"policy_denied\"}}");
        }
    }

    if (response != NULL) {
        (void)transport_send_framed((client_fd == STDIN_FILENO) ? STDOUT_FILENO : client_fd, response);
    }

    if (pchk == POLICY_ALLOW && policy_src == POLICY_SOURCE_PREAPPROVE) {
        source_label = "preapprove";
    } else if (pchk == POLICY_ALLOW && policy_src == POLICY_SOURCE_SESSION_GRANT) {
        source_label = "session-grant";
    } else if (pchk == POLICY_ALLOW) {
        source_label = "prompt";
    } else {
        source_label = "no-match";
    }

    (void)snprintf(activity,
                   sizeof(activity),
                   "%s %s(%s) %s:%s",
                   caller.caller_id,
                   method,
                   role_name,
                   verdict,
                   source_label);

    if (cb != NULL) {
        cb(activity, cb_data);
    }

    free(request);
    free(response);
    if (client_fd != STDIN_FILENO) {
        close(client_fd);
    }
    return 1;
}

void server_stop(server_ctx_t *ctx) {
    if (ctx == NULL) {
        return;
    }

    if (ctx->listen_fd >= 0) {
        if (ctx->listen_mode == NSIGNER_LISTEN_UNIX || ctx->listen_mode == NSIGNER_LISTEN_TCP) {
            close(ctx->listen_fd);
        }
        ctx->listen_fd = -1;
    }
    ctx->running = 0;
}
