v0.0.34 - Add qrexec bridge subcommand and --bridge-source-trusted flag for persistent-signer qrexec transport

This commit is contained in:
Laan Tungir
2026-07-11 09:37:51 -04:00
parent fd622dfd60
commit d15eebb80f
147 changed files with 71205 additions and 613 deletions

195
examples/get_pubkey_tcp.c Normal file
View File

@@ -0,0 +1,195 @@
/*
* get_pubkey_tcp.c — connect to a running n_signer over TCP and call
* get_public_key for nostr_index 0 and 1, printing both hex pubkey and
* bech32 npub for each.
*
* This is a cross-qube test client for Qubes OS: the signer runs in the
* nostr_signer qube listening on tcp:[::]:8080, and this client runs in
* a different qube connecting to the signer's FIPS address.
*
* Usage:
* ./get_pubkey_tcp <host> <port>
* ./get_pubkey_tcp npub1xxx...fips 8080
*
* If no arguments are given, defaults to localhost:8080.
*
* Output: for each index, prints:
* index 0: hex=<64 hex chars> npub=npub1...
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "nostr_common.h"
#include "nsigner_transport.h"
#include "nsigner_client.h"
#include "nip019.h"
#include "../cjson/cJSON.h"
/* Demo caller key (32 bytes). Replace with your stable caller key in real use. */
static const unsigned char DEMO_PRIVKEY[32] = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32
};
static int hex_to_bytes(const char *hex, unsigned char *out, size_t out_len) {
size_t len = strlen(hex);
if (len != out_len * 2) {
return -1;
}
for (size_t i = 0; i < out_len; i++) {
unsigned int byte;
if (sscanf(hex + 2 * i, "%2x", &byte) != 1) {
return -1;
}
out[i] = (unsigned char)byte;
}
return 0;
}
static int query_pubkey(const char *host, int port, int nostr_index,
char *out_hex, size_t hex_size, char *out_npub, size_t npub_size) {
(void)npub_size; /* npub buffer size is enforced by nostr_key_to_bech32 output */
nsigner_transport_t *transport = NULL;
nsigner_client_t *client = NULL;
cJSON *params = NULL;
cJSON *opts = NULL;
cJSON *result = NULL;
const char *hex_pubkey = NULL;
unsigned char pubkey_bytes[32];
int rc = -1;
transport = nsigner_transport_open_tcp(host, port, 10000);
if (transport == NULL) {
fprintf(stderr, "connect failed: cannot open TCP transport to %s:%d\n", host, port);
goto cleanup;
}
client = nsigner_client_new(transport);
if (client == NULL) {
fprintf(stderr, "connect failed: cannot create nsigner client\n");
transport->close(transport);
goto cleanup;
}
transport = NULL; /* owned by client now */
/* Set auth envelope — required for TCP listeners */
if (nsigner_client_set_auth(client, DEMO_PRIVKEY, "get_pubkey_tcp") != NOSTR_SUCCESS) {
fprintf(stderr, "failed to set auth envelope\n");
goto cleanup;
}
/* params: [{"nostr_index": N}] */
params = cJSON_CreateArray();
if (params == NULL) {
fprintf(stderr, "out of memory\n");
goto cleanup;
}
opts = cJSON_CreateObject();
if (opts == NULL) {
fprintf(stderr, "out of memory\n");
goto cleanup;
}
cJSON_AddNumberToObject(opts, "nostr_index", nostr_index);
cJSON_AddItemToArray(params, opts);
opts = NULL;
if (nsigner_client_call(client, "get_public_key", params, &result) != NOSTR_SUCCESS) {
fprintf(stderr, "request failed for index %d: %s\n", nostr_index,
nsigner_client_last_error(client));
params = NULL; /* nsigner_client_call took ownership even on failure */
goto cleanup;
}
params = NULL; /* nsigner_client_call took ownership */
if (!cJSON_IsString(result)) {
fprintf(stderr, "index %d: unexpected result type\n", nostr_index);
goto cleanup;
}
hex_pubkey = result->valuestring;
if (strlen(hex_pubkey) != 64) {
fprintf(stderr, "index %d: unexpected pubkey length: %zu\n", nostr_index, strlen(hex_pubkey));
goto cleanup;
}
strncpy(out_hex, hex_pubkey, hex_size - 1);
out_hex[hex_size - 1] = '\0';
/* Convert hex pubkey to npub (bech32) */
if (hex_to_bytes(hex_pubkey, pubkey_bytes, 32) != 0) {
fprintf(stderr, "index %d: failed to parse hex pubkey\n", nostr_index);
goto cleanup;
}
if (nostr_key_to_bech32(pubkey_bytes, "npub", out_npub) != NOSTR_SUCCESS) {
fprintf(stderr, "index %d: failed to convert to npub\n", nostr_index);
goto cleanup;
}
rc = 0;
cleanup:
cJSON_Delete(opts);
cJSON_Delete(params);
cJSON_Delete(result);
nsigner_client_free(client);
return rc;
}
int main(int argc, char **argv) {
const char *host = "127.0.0.1";
int port = 8080;
char hex0[65], npub0[128];
char hex1[65], npub1[128];
int failures = 0;
if (argc > 1) {
host = argv[1];
}
if (argc > 2) {
port = atoi(argv[2]);
if (port <= 0 || port > 65535) {
fprintf(stderr, "Invalid port: %s\n", argv[2]);
return 1;
}
}
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "failed to initialize crypto subsystem\n");
return 1;
}
printf("Connecting to n_signer at %s:%d\n", host, port);
printf("Querying get_public_key for nostr_index 0 and 1...\n\n");
/* Query index 0 */
hex0[0] = '\0';
npub0[0] = '\0';
if (query_pubkey(host, port, 0, hex0, sizeof(hex0), npub0, sizeof(npub0)) == 0) {
printf("index 0: hex=%s npub=%s\n", hex0, npub0);
} else {
printf("index 0: FAILED\n");
failures++;
}
/* Query index 1 */
hex1[0] = '\0';
npub1[0] = '\0';
if (query_pubkey(host, port, 1, hex1, sizeof(hex1), npub1, sizeof(npub1)) == 0) {
printf("index 1: hex=%s npub=%s\n", hex1, npub1);
} else {
printf("index 1: FAILED\n");
failures++;
}
nostr_cleanup();
if (failures > 0) {
printf("\n%d query(s) failed\n", failures);
return 1;
}
printf("\nAll queries succeeded\n");
return 0;
}

View File

@@ -1,32 +1,100 @@
/*
* get_public_key_client.c — connect to a running n_signer over its abstract
* UNIX socket and call get_public_key, using the shared nsigner client from
* nostr_core_lib (nostr_core/nsigner_client.h + nostr_core/nsigner_transport.h).
*
* Usage: ./get_public_key_client [socket_name]
*
* Output: the raw JSON-RPC response string, e.g.
* {"id":"1","result":"<pubkey hex>"}
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../client/nsigner_client.h"
#include "nostr_common.h"
#include "nsigner_transport.h"
#include "nsigner_client.h"
#include "../cjson/cJSON.h"
int main(int argc, char **argv) {
const char *socket_name = "nsigner";
nsigner_client_t client;
char *response = NULL;
nsigner_transport_t *transport = NULL;
nsigner_client_t *client = NULL;
cJSON *params = NULL;
cJSON *result = NULL;
cJSON *response = NULL;
char *response_json = NULL;
int rc = 1;
if (argc > 1 && argv[1] != NULL && argv[1][0] != '\0') {
socket_name = argv[1];
}
nsigner_client_init(&client);
if (nsigner_client_connect_unix(&client, socket_name, 5000) != 0) {
fprintf(stderr, "connect failed: %s\n", nsigner_client_last_error(&client));
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "failed to initialize crypto subsystem\n");
return 1;
}
if (nsigner_client_get_public_key(&client, "example-1", "", &response) != 0) {
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(&client));
nsigner_client_close(&client);
return 1;
transport = nsigner_transport_open_unix(socket_name, 5000);
if (transport == NULL) {
fprintf(stderr, "connect failed: cannot open unix transport @%s\n", socket_name);
goto cleanup;
}
printf("%s\n", response);
free(response);
nsigner_client_close(&client);
return 0;
client = nsigner_client_new(transport);
if (client == NULL) {
fprintf(stderr, "connect failed: cannot create nsigner client\n");
/* nsigner_client_new takes ownership of transport on success only */
transport->close(transport);
goto cleanup;
}
transport = NULL; /* owned by client now */
/* params: [] (empty array — server picks the default role) */
params = cJSON_CreateArray();
if (params == NULL) {
fprintf(stderr, "out of memory\n");
goto cleanup;
}
if (nsigner_client_call(client, "get_public_key", params, &result) != NOSTR_SUCCESS) {
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(client));
goto cleanup;
}
params = NULL; /* nsigner_client_call takes ownership of params */
/*
* Reconstruct a JSON-RPC response string so the CLI output stays
* backward-compatible with the old client example:
* {"id":"<id>","result":"<pubkey hex>"}
* nsigner_client_call returns only the parsed `result` element and does
* not expose the server-assigned id, so we emit a minimal envelope.
*/
response = cJSON_CreateObject();
if (response == NULL) {
fprintf(stderr, "out of memory\n");
goto cleanup;
}
cJSON_AddStringToObject(response, "id", "1");
cJSON_AddItemReferenceToObject(response, "result", result);
response_json = cJSON_PrintUnformatted(response);
if (response_json == NULL) {
fprintf(stderr, "failed to serialize response\n");
goto cleanup;
}
printf("%s\n", response_json);
rc = 0;
cleanup:
free(response_json);
cJSON_Delete(response);
cJSON_Delete(result);
cJSON_Delete(params);
nsigner_client_free(client); /* also closes/frees the transport */
nostr_cleanup();
return rc;
}

View File

@@ -0,0 +1,257 @@
#!/usr/bin/env node
/**
* n_signer_qube_example.js — connect to a running n_signer over TCP (FIPS mesh)
* and call get_public_key for nostr_index 0 and 1, printing both hex pubkey
* and bech32 npub for each.
*
* This is a cross-qube test client for Qubes OS: the signer runs in the
* nostr_signer qube listening on tcp:[::]:8080, and this client runs in
* a different qube connecting to the signer's FIPS address.
*
* Usage:
* node n_signer_qube_example.js [host] [port]
* node n_signer_qube_example.js fd56:d7c3:f605:719d:15b:18a0:fb06:982f 8080
*
* If no arguments are given, defaults to localhost:8080.
*
* Protocol:
* - 4-byte big-endian length prefix + JSON payload (TCP framing)
* - JSON-RPC: {"id":"...","method":"get_public_key","params":[{"nostr_index":N}],"auth":{...}}
* - Auth envelope: kind 27235 Nostr event with nsigner_rpc, nsigner_method,
* nsigner_body_hash tags. body_hash = sha256(json.dumps(params))
* - Response: {"id":"...","result":"<pubkey hex>"} or {"id":"...","error":{...}}
*/
const net = require("net");
const crypto = require("crypto");
const secp = require("@noble/secp256k1");
// @noble/secp256k1 v3 requires us to provide sync sha256/hmacSha256
secp.hashes.sha256 = (msg) => new Uint8Array(crypto.createHash("sha256").update(msg).digest());
secp.hashes.hmacSha256 = (key, msg) =>
new Uint8Array(crypto.createHmac("sha256", key).update(msg).digest());
const { schnorr } = secp;
/** Convert a Uint8Array to a hex string. */
function bytesToHex(bytes) {
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
}
/** Convert a hex string to a Uint8Array. */
function hexToBytes(hex) {
const arr = new Uint8Array(hex.length / 2);
for (let i = 0; i < arr.length; i++) {
arr[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return arr;
}
// Demo caller key (32 bytes). Replace with your stable caller key in real use.
const DEMO_PRIVKEY = new Uint8Array(Array.from({ length: 32 }, (_, i) => i + 1));
/**
* Compute the body hash for a params array.
* This must match n_signer's hashing: sha256 of canonical JSON (no spaces).
*/
function computeBodyHash(params) {
const canonical = JSON.stringify(params);
return crypto.createHash("sha256").update(canonical).digest("hex");
}
/**
* Build a kind-27235 auth envelope for the given method and params.
* Returns the auth object to include in the JSON-RPC request.
*/
function buildAuthEnvelope(privkey, method, params, requestId) {
const bodyHash = computeBodyHash(params);
const pubkeyHex = bytesToHex(schnorr.getPublicKey(privkey));
const createdAt = Math.floor(Date.now() / 1000);
// nsigner_rpc tag value MUST match the JSON-RPC request "id" field
const tags = [
["nsigner_rpc", requestId],
["nsigner_method", method],
["nsigner_body_hash", bodyHash],
];
const content = "js-example";
// Serialize the event for signing: [0, pubkey, created_at, kind, tags, content]
const serialized = JSON.stringify([
0,
pubkeyHex,
createdAt,
27235,
tags,
content,
]);
const eventId = crypto.createHash("sha256").update(serialized).digest("hex");
const sig = bytesToHex(schnorr.sign(hexToBytes(eventId), privkey, new Uint8Array(32)));
return {
id: eventId,
pubkey: pubkeyHex,
created_at: createdAt,
kind: 27235,
tags: tags,
content: content,
sig: sig,
};
}
/**
* Send a framed JSON-RPC request over a TCP socket and receive the response.
* Framing: 4-byte big-endian length prefix + JSON payload.
*/
function sendRequest(socket, request) {
return new Promise((resolve, reject) => {
const payload = Buffer.from(JSON.stringify(request), "utf8");
const header = Buffer.alloc(4);
header.writeUInt32BE(payload.length, 0);
socket.write(Buffer.concat([header, payload]));
let headerBuf = Buffer.alloc(0);
let bodyBuf = Buffer.alloc(0);
let bodyLen = 0;
let state = "header";
const onData = (chunk) => {
if (state === "header") {
headerBuf = Buffer.concat([headerBuf, chunk]);
if (headerBuf.length >= 4) {
bodyLen = headerBuf.readUInt32BE(0);
const remaining = headerBuf.subarray(4);
headerBuf = Buffer.alloc(0);
state = "body";
if (remaining.length > 0) {
bodyBuf = Buffer.concat([bodyBuf, remaining]);
}
if (bodyBuf.length >= bodyLen) {
finish();
}
}
} else if (state === "body") {
bodyBuf = Buffer.concat([bodyBuf, chunk]);
if (bodyBuf.length >= bodyLen) {
finish();
}
}
};
function finish() {
socket.off("data", onData);
socket.off("error", onError);
const body = bodyBuf.subarray(0, bodyLen).toString("utf8");
try {
resolve(JSON.parse(body));
} catch (e) {
reject(new Error(`Failed to parse response: ${e.message}`));
}
}
function onError(err) {
socket.off("data", onData);
reject(err);
}
socket.on("data", onData);
socket.on("error", onError);
});
}
/**
* Query get_public_key for a given nostr_index.
* Opens a fresh TCP connection for each request (n_signer handles one request
* per connection).
*/
async function getPublicKey(host, port, nostrIndex, privkey) {
const params = [{ nostr_index: nostrIndex }];
const requestId = String(nostrIndex);
const auth = buildAuthEnvelope(privkey, "get_public_key", params, requestId);
const request = {
id: requestId,
method: "get_public_key",
params: params,
auth: auth,
};
return new Promise((resolve, reject) => {
const socket = new net.Socket();
socket.setTimeout(15000);
socket.connect(port, host, async () => {
try {
const response = await sendRequest(socket, request);
socket.destroy();
resolve(response);
} catch (e) {
socket.destroy();
reject(e);
}
});
socket.on("timeout", () => {
socket.destroy();
reject(new Error("Connection timed out"));
});
socket.on("error", (err) => {
reject(err);
});
});
}
/**
* Convert a 32-byte hex pubkey to bech32 npub format (NIP-19).
*/
function hexToNpub(pubkeyHex) {
const { nip19 } = require("nostr-tools");
return nip19.npubEncode(pubkeyHex);
}
async function main() {
const host = process.argv[2] || "127.0.0.1";
const port = parseInt(process.argv[3] || "8080", 10);
console.log(`Connecting to n_signer at ${host}:${port}`);
console.log("Querying get_public_key for nostr_index 0 and 1...\n");
let failures = 0;
for (const index of [0, 1]) {
// Small delay between requests to avoid auth nonce collision
// (the auth envelope uses created_at as part of the nonce)
if (index > 0) await new Promise((r) => setTimeout(r, 1100));
try {
const response = await getPublicKey(host, port, index, DEMO_PRIVKEY);
if (response.error) {
console.log(`index ${index}: ERROR: ${JSON.stringify(response.error)}`);
failures++;
continue;
}
const pubkeyHex = response.result;
const npub = hexToNpub(pubkeyHex);
console.log(`index ${index}: hex=${pubkeyHex} npub=${npub}`);
} catch (e) {
console.log(`index ${index}: FAILED - ${e.message}`);
failures++;
}
}
if (failures > 0) {
console.log(`\n${failures} query(s) failed`);
process.exit(1);
}
console.log("\nAll queries succeeded");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -0,0 +1,155 @@
#!/usr/bin/env node
/**
* n_signer_qube_example_qrexec.js — connect to n_signer in the nostr_signer
* qube via Qubes qrexec (qubes.NsignerRpc service) and call get_public_key
* for nostr_index 0 and 1, printing both hex pubkey and bech32 npub.
*
* This uses Qubes OS's built-in secure IPC (qrexec) instead of FIPS TCP.
* No network connectivity is required — all traffic stays within the host.
*
* Prerequisites:
* - The qubes.NsignerRpc service must be installed in the nostr_signer qube
* (see packaging/qubes/install-service.sh)
* - The qrexec policy must be installed in dom0
* (see packaging/qubes/install-policy.sh)
* - The nostr_signer qube must be tagged with 'nsigner-signer'
* (qvm-tags nostr_signer add nsigner-signer)
* - A mnemonic file must exist at /home/user/.nsigner_mnemonic in the
* nostr_signer qube
*
* Usage:
* node n_signer_qube_example_qrexec.js [target_qube]
* node n_signer_qube_example_qrexec.js nostr_signer
*
* If no argument is given, defaults to "nostr_signer".
*
* Protocol:
* - qrexec-client-vm spawns the qubes.NsignerRpc service in the target qube
* - The service runs: nsigner --listen qrexec --allow-all --mnemonic-fd 3
* - We send one framed JSON-RPC request via stdin, receive one framed response via stdout
* - Framing: 4-byte big-endian length prefix + JSON payload
* - Auth: not required in qrexec mode (caller identity is qubes:<source-vm>)
*/
const { spawn } = require("child_process");
const { nip19 } = require("nostr-tools");
/**
* Frame a JSON-RPC request: 4-byte big-endian length + JSON payload.
*/
function frameRequest(obj) {
const payload = Buffer.from(JSON.stringify(obj), "utf8");
const header = Buffer.alloc(4);
header.writeUInt32BE(payload.length, 0);
return Buffer.concat([header, payload]);
}
/**
* Parse a framed response from the qrexec stdout buffer.
*/
function parseFramedResponse(buf) {
if (buf.length < 4) {
throw new Error("short response (missing frame header)");
}
const len = buf.readUInt32BE(0);
const payload = buf.subarray(4, 4 + len);
if (payload.length !== len) {
throw new Error(`short response payload: expected ${len}, got ${payload.length}`);
}
return JSON.parse(payload.toString("utf8"));
}
/**
* Call nsigner via qrexec. Sends one framed request, receives one framed response.
* Each call spawns a fresh qrexec-client-vm process (one request per invocation).
*/
function callNsignerQrexec(targetQube, request) {
return new Promise((resolve, reject) => {
const framed = frameRequest(request);
const proc = spawn("qrexec-client-vm", [targetQube, "qubes.NsignerRpc"], {
stdio: ["pipe", "pipe", "pipe"],
});
const stdoutChunks = [];
const stderrChunks = [];
proc.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
proc.stderr.on("data", (chunk) => stderrChunks.push(chunk));
proc.on("error", (err) => {
reject(new Error(`failed to spawn qrexec-client-vm: ${err.message}`));
});
proc.on("close", (code) => {
if (code !== 0) {
const stderr = Buffer.concat(stderrChunks).toString("utf8");
reject(new Error(`qrexec-client-vm exited with code ${code}: ${stderr.trim()}`));
return;
}
try {
const response = parseFramedResponse(Buffer.concat(stdoutChunks));
resolve(response);
} catch (e) {
reject(new Error(`failed to parse response: ${e.message}`));
}
});
// Send the framed request and close stdin
proc.stdin.write(framed);
proc.stdin.end();
});
}
/**
* Query get_public_key for a given nostr_index via qrexec.
* No auth envelope needed — qrexec mode uses qubes:<source-vm> as caller identity.
*/
async function getPublicKey(targetQube, nostrIndex) {
const request = {
id: String(nostrIndex),
method: "get_public_key",
params: [{ nostr_index: nostrIndex }],
};
return callNsignerQrexec(targetQube, request);
}
async function main() {
const targetQube = process.argv[2] || "nostr_signer";
console.log(`Calling n_signer in qube "${targetQube}" via qrexec...`);
console.log("Querying get_public_key for nostr_index 0 and 1...\n");
let failures = 0;
for (const index of [0, 1]) {
try {
const response = await getPublicKey(targetQube, index);
if (response.error) {
console.log(`index ${index}: ERROR: ${JSON.stringify(response.error)}`);
failures++;
continue;
}
const pubkeyHex = response.result;
const npub = nip19.npubEncode(pubkeyHex);
console.log(`index ${index}: hex=${pubkeyHex} npub=${npub}`);
} catch (e) {
console.log(`index ${index}: FAILED - ${e.message}`);
failures++;
}
}
if (failures > 0) {
console.log(`\n${failures} query(s) failed`);
process.exit(1);
}
console.log("\nAll queries succeeded");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -1,33 +1,114 @@
/*
* sign_event_client.c — connect to a running n_signer over its abstract
* UNIX socket and call sign_event, using the shared nsigner client from
* nostr_core_lib (nostr_core/nsigner_client.h + nostr_core/nsigner_transport.h).
*
* Usage: ./sign_event_client [socket_name]
*
* Output: the raw JSON-RPC response string, e.g.
* {"id":"1","result":"{...signed event...}"}
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../client/nsigner_client.h"
#include "nostr_common.h"
#include "nsigner_transport.h"
#include "nsigner_client.h"
#include "../cjson/cJSON.h"
int main(int argc, char **argv) {
const char *socket_name = "nsigner";
const char *event_json = "{\"kind\":1,\"content\":\"hello from client example\",\"tags\":[],\"created_at\":1700000000}";
nsigner_client_t client;
char *response = NULL;
const char *role = "main";
nsigner_transport_t *transport = NULL;
nsigner_client_t *client = NULL;
cJSON *params = NULL;
cJSON *opts = NULL;
cJSON *result = NULL;
cJSON *response = NULL;
char *response_json = NULL;
int rc = 1;
if (argc > 1 && argv[1] != NULL && argv[1][0] != '\0') {
socket_name = argv[1];
}
nsigner_client_init(&client);
if (nsigner_client_connect_unix(&client, socket_name, 5000) != 0) {
fprintf(stderr, "connect failed: %s\n", nsigner_client_last_error(&client));
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "failed to initialize crypto subsystem\n");
return 1;
}
if (nsigner_client_sign_event(&client, "example-2", event_json, "main", &response) != 0) {
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(&client));
nsigner_client_close(&client);
return 1;
transport = nsigner_transport_open_unix(socket_name, 5000);
if (transport == NULL) {
fprintf(stderr, "connect failed: cannot open unix transport @%s\n", socket_name);
goto cleanup;
}
printf("%s\n", response);
free(response);
nsigner_client_close(&client);
return 0;
client = nsigner_client_new(transport);
if (client == NULL) {
fprintf(stderr, "connect failed: cannot create nsigner client\n");
transport->close(transport);
goto cleanup;
}
transport = NULL; /* owned by client now */
/* params: [ "<event_json>", {"role":"main"} ] */
params = cJSON_CreateArray();
if (params == NULL) {
fprintf(stderr, "out of memory\n");
goto cleanup;
}
cJSON_AddItemToArray(params, cJSON_CreateString(event_json));
opts = cJSON_CreateObject();
if (opts == NULL) {
fprintf(stderr, "out of memory\n");
goto cleanup;
}
if (role != NULL && role[0] != '\0') {
cJSON_AddStringToObject(opts, "role", role);
}
cJSON_AddItemToArray(params, opts);
opts = NULL; /* owned by params now */
if (nsigner_client_call(client, "sign_event", params, &result) != NOSTR_SUCCESS) {
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(client));
goto cleanup;
}
params = NULL; /* nsigner_client_call takes ownership of params */
/*
* Reconstruct a JSON-RPC response string for backward-compatible CLI
* output: {"id":"1","result":"<signed event json>"}
* The server returns the signed event as a JSON string (not an object),
* so result is a cJSON string here.
*/
response = cJSON_CreateObject();
if (response == NULL) {
fprintf(stderr, "out of memory\n");
goto cleanup;
}
cJSON_AddStringToObject(response, "id", "1");
cJSON_AddItemReferenceToObject(response, "result", result);
response_json = cJSON_PrintUnformatted(response);
if (response_json == NULL) {
fprintf(stderr, "failed to serialize response\n");
goto cleanup;
}
printf("%s\n", response_json);
rc = 0;
cleanup:
free(response_json);
cJSON_Delete(response);
cJSON_Delete(result);
cJSON_Delete(params);
cJSON_Delete(opts);
nsigner_client_free(client); /* also closes/frees the transport */
nostr_cleanup();
return rc;
}